# HG changeset patch # User cmlenz # Date 1118933391 0 # Node ID 21aa17f975228cad5e741a1dff1c0d07661d88e4 # Parent fd802a55be551a869feb2e34b412da3db28f999e Initial code for build master and slave... these don't do a lot yet. diff --git a/bitten/master.py b/bitten/master.py new file mode 100644 --- /dev/null +++ b/bitten/master.py @@ -0,0 +1,74 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# +# Bitten is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Trac is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# Author: Christopher Lenz + +import asyncore +import getopt +import os.path +import sys + +from bitten.util import beep +from bitten.util.xmlio import Element, parse_xml + + +class BittenProfile(beep.Profile): + URI = 'http://bitten.cmlenz.net/beep-profile/' + + def __init__(self): + beep.Profile.__init__(self) + + def handle_msg(self, msgno, msg): + assert msg.get_content_type() == beep.BEEP_XML + elem = parse_xml(msg.get_payload()) + + if elem.tagname == 'register': + platform, os, os_family, os_version = None, None, None, None + for child in elem['*']: + if child.tagname == 'platform': + platform = child.gettext() + elif child.tagname == 'os': + os = child.gettext() + os_family = child.family + os_version = child.version + rpy = beep.MIMEMessage(Element('ok'), beep.BEEP_XML) + self.channel.send_rpy(msgno, rpy) + print 'Connected to %s (%s running %s %s [%s])' \ + % (elem.name, platform, os, os_version, os_family) + + +if __name__ == '__main__': + options, args = getopt.getopt(sys.argv[1:], 'p:', ['port=']) + if len(args) < 1: + print>>sys.stderr, 'usage: %s [options] ENV_PATH' % os.path.basename(sys.argv[0]) + print>>sys.stderr + print>>sys.stderr, 'Valid options:' + print>>sys.stderr, ' -p [--port] arg\tport number to use (default: 7633)' + sys.exit(2) + + if len(args) > 1: + port = int(args[1]) + else: + port = 7633 + + listener = beep.Listener('localhost', port) + listener.profiles[BittenProfile.URI] = BittenProfile() + try: + asyncore.loop() + except KeyboardInterrupt: + pass diff --git a/bitten/slave.py b/bitten/slave.py new file mode 100644 --- /dev/null +++ b/bitten/slave.py @@ -0,0 +1,115 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# +# Bitten is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Trac is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# Author: Christopher Lenz + +import asyncore +import getopt +import os +import sys +import time + +from bitten.util import beep +from bitten.util.xmlio import Element, parse_xml + + +class Slave(beep.Initiator): + + channelno = None # The channel number used by the bitten profile + + def channel_started(self, channelno, profile_uri): + if profile_uri == BittenProfileHandler.URI: + self.channelno = channelno + + def greeting_received(self, profiles): + if BittenProfileHandler.URI in profiles: + self.channels[0].profile.send_start([BittenProfileHandler], + handle_ok=self.channel_started) + +class BittenProfileHandler(beep.Profile): + """Handles communication on the Bitten profile from the client perspective. + """ + URI = 'http://bitten.cmlenz.net/beep-profile/' + + def handle_connect(self): + """Register with the build master.""" + sysname, nodename, release, version, machine = os.uname() + print 'Registering with build master as %s' % nodename + register = Element('register', name=nodename)[ + Element('platform')[machine], + Element('os', family=os.name, version=release)[sysname] + ] + def handle_reply(cmd, msgno, msg): + if cmd == 'RPY': + print 'Registration successful' + else: + if msg.get_content_type() == beep.BEEP_XML: + elem = parse_xml(msg.get_payload()) + if elem.tagname == 'error': + raise beep.TerminateSession, \ + '%s (%s)' % (elem.gettext(), elem.code) + raise beep.TerminateSession, 'Registration failed!' + self.channel.send_msg(beep.MIMEMessage(register, beep.BEEP_XML), + handle_reply) + + def handle_msg(self, msgno, msg): + # TODO: Handle build initiation requests + pass + + +if __name__ == '__main__': + options, args = getopt.getopt(sys.argv[1:], 'vq', ['verbose', 'qiuet']) + if len(args) < 1: + print>>sys.stderr, 'Usage: %s [options] host [port]' % sys.argv[0] + print>>sys.stderr + print>>sys.stderr, 'Valid options:' + print>>sys.stderr, ' -q [--quiet]\tprint as little as possible' + print>>sys.stderr, ' -v [--verbose]\tprint as much as possible' + sys.exit(2) + + host = args[0] + if len(args) > 1: + port = int(args[1]) + else: + port = 7633 + + verbose = False + quiet = False + for opt, arg in options: + if opt in ('-v', '--verbose'): + verbose = True + elif opt in ('-q', '--quiet'): + quiet = True + + slave = Slave(host, port) + try: + try: + asyncore.loop() + except KeyboardInterrupt, beep.TerminateSession: + def handle_ok(): + raise asyncore.ExitNow, 'Session terminated' + def handle_error(code, message): + print>>sys.stderr, \ + 'Build master refused to terminate session (%d): %s' \ + % (code, message) + slave.channels[0].profile.send_close(slave.channelno) + slave.channels[0].profile.send_close(handle_ok=handle_ok, + handle_error=handle_error) + time.sleep(.25) + except beep.TerminateSession, e: + print e diff --git a/bitten/util/beep.py b/bitten/util/beep.py --- a/bitten/util/beep.py +++ b/bitten/util/beep.py @@ -45,6 +45,10 @@ """Generic root class for BEEP exceptions.""" +class TerminateSession(Exception): + """Signal termination of a session.""" + + class Listener(asyncore.dispatcher): """BEEP peer in the listener role. @@ -98,13 +102,16 @@ pass def handle_error(self): + """Called by asyncore when an exception is raised.""" t, v = sys.exc_info()[:2] - if t is SystemExit: + if t is TerminateSession: raise t, v - else: - asynchat.async_chat.handle_error(self) + asynchat.async_chat.handle_error(self) def collect_incoming_data(self, data): + """Called by async_chat when data is received. + + Buffer the data and wait until a terminator is found.""" self.inbuf.append(data) def found_terminator(self): @@ -373,7 +380,7 @@ assert message.get_content_type() == BEEP_XML elem = parse_xml(message.get_payload()) - if elem.name == 'start': + if elem.tagname == 'start': for profile in elem['profile']: if profile.uri in self.session.profiles.keys(): print 'Start channel %s for profile <%s>' % (elem.number, @@ -388,7 +395,7 @@ self.send_error(msgno, 550, 'All requested profiles are unsupported') - elif elem.name == 'close': + elif elem.tagname == 'close': channelno = int(elem.number) if channelno == 0: if len(self.session.channels) > 1: @@ -408,7 +415,7 @@ assert message.get_content_type() == BEEP_XML elem = parse_xml(message.get_payload()) - if elem.name == 'greeting': + if elem.tagname == 'greeting': if isinstance(self.session, Initiator): profiles = [profile.uri for profile in elem['profile']] self.session.greeting_received(profiles) @@ -422,7 +429,7 @@ # TODO: Terminate the session, I guess assert message.get_content_type() == BEEP_XML elem = parse_xml(message.get_payload()) - assert elem.name == 'error' + assert elem.tagname == 'error' print elem.code def send_close(self, channelno=0, code=200, handle_ok=None, diff --git a/bitten/util/xmlio.py b/bitten/util/xmlio.py --- a/bitten/util/xmlio.py +++ b/bitten/util/xmlio.py @@ -67,14 +67,14 @@ >>> print Element('foo')['Hello ', Element('b')['world']] Hello world """ - __slots__ = ['name', 'attrs', 'children'] + __slots__ = ['tagname', 'attrs', 'children'] - def __init__(self, name, **attrs): + def __init__(self, tagname, **attrs): """Create an XML element using the specified tag name. All keyword arguments are handled as attributes of the element. """ - self.name = name + self.tagname = tagname self.attrs = attrs self.children = [] @@ -89,7 +89,7 @@ """Return a string representation of the XML element.""" buf = StringIO() buf.write('<') - buf.write(self.name) + buf.write(self.tagname) for name, value in self.attrs.items(): buf.write(' ') buf.write(name) @@ -104,7 +104,7 @@ else: buf.write(self._escape_text(child)) buf.write('') else: buf.write('/>') @@ -133,7 +133,7 @@ def __init__(self, node): self.node = node - name = property(fget=lambda self: self.node.tagName) + tagname = property(fget=lambda self: self.node.tagName) def __getattr__(self, name): return self.node.getAttribute(name) diff --git a/scripts/echoclient.py b/scripts/echoclient.py deleted file mode 100644 --- a/scripts/echoclient.py +++ /dev/null @@ -1,78 +0,0 @@ -import asyncore -import sys -import time - -from bitten.util.beep import Initiator, MIMEMessage, Profile -from bitten.util.xmlio import Element, parse_xml - - -class StdinChannel(asyncore.file_dispatcher): - - def __init__(self, handle_read): - asyncore.file_dispatcher.__init__(self, sys.stdin.fileno()) - self.read_handler = handle_read - - def readable(self): - return True - - def handle_read(self): - data = self.recv(8192) - self.read_handler(data) - - def writable(self): - return False - - -class EchoProfile(Profile): - URI = 'http://beepcore.org/beep/ECHO' - - def handle_rpy(self, msgno, message): - print '\x1b[31m' + message.get_payload().rstrip() + '\x1b[0m' - - -class EchoClient(Initiator): - - channel = None - - def greeting_received(self, profiles): - def handle_ok(channelno, uri): - print 'Channel %d started for profile %s...' % (channelno, uri) - self.channel = channelno - def handle_error(code, message): - print>>sys.stderr, 'Error %d: %s' % (code, message) - if EchoProfile.URI in profiles: - self.channels[0].profile.send_start([EchoProfile], - handle_ok=handle_ok, - handle_error=handle_error) - - -if __name__ == '__main__': - host = 'localhost' - port = 8000 - if len(sys.argv) > 1: - host = sys.argv[1] - if len(sys.argv) > 2: - port = int(sys.argv[2]) - - client = EchoClient(host, port) - def handle_input(data): - message = MIMEMessage(data, 'text/plain') - client.channels[client.channel].send_msg(message) - stdin = StdinChannel(handle_input) - try: - while client: - try: - asyncore.loop() - except KeyboardInterrupt: - mgmt = client.channels[0].profile - def handle_ok(): - raise asyncore.ExitNow, 'Session terminated' - def handle_error(code, message): - print>>sys.stderr, \ - 'Peer refused to terminate session (%d): %s' \ - % (code, message) - mgmt.send_close(client.channel) - mgmt.send_close(handle_ok=handle_ok, handle_error=handle_error) - time.sleep(.25) - except asyncore.ExitNow, e: - print e