changeset 287:6abd43d0cd8a

The build slave now stores snapshot archives and the corresponding work directories in project folders of the main work folder, to keep build configurations from different projects that share the same name separate. This also requires transmitting the project name (simply the name of the environment directory) with the build initiation.
author cmlenz
date Sat, 15 Oct 2005 13:39:23 +0000
parents 4fabcaf75787
children a5ed3341d9a9
files bitten/master.py bitten/slave.py bitten/util/beep.py bitten/util/xmlio.py
diffstat 4 files changed, 85 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -198,8 +198,9 @@
             else:
                 self.send_snapshot(queue, build, snapshot)
 
-        self.channel.send_msg(beep.Payload(config.recipe),
-                              handle_reply=handle_reply)
+        xml = xmlio.parse(config.recipe)
+        xml.attr['project'] = os.path.basename(queue.env.path)
+        self.channel.send_msg(beep.Payload(xml), handle_reply=handle_reply)
 
     def send_snapshot(self, queue, build, snapshot):
         timestamp_delta = 0
@@ -418,10 +419,16 @@
             host = ip
 
     envs = []
+    names = set()
     for arg in args:
         if not os.path.isdir(arg):
             log.warning('Ignoring %s: not a directory', arg)
             continue
+        name = os.path.basename(arg)
+        if name in names:
+            log.warning('Ignoring %s: duplicate project name "%s"', arg, name)
+            continue
+        names.add(name)
         env = Environment(arg)
         if BuildSystem(env):
             if env.needs_upgrade():
--- a/bitten/slave.py
+++ b/bitten/slave.py
@@ -60,7 +60,7 @@
 
     def handle_connect(self):
         """Register with the build master."""
-        self.recipe_xml = None
+        self.build_xml = None
 
         def handle_reply(cmd, msgno, ansno, payload):
             if cmd == 'ERR':
@@ -99,34 +99,40 @@
             elem = xmlio.parse(payload.body)
             if elem.name == 'build':
                 # Received a build request
-                self.recipe_xml = elem
+                self.build_xml = elem
                 xml = xmlio.Element('proceed')
                 self.channel.send_rpy(msgno, beep.Payload(xml))
 
         elif payload.content_type == 'application/zip':
             # Received snapshot archive for build
+            project_name = self.build_xml.attr.get('project', 'default')
+            project_dir = os.path.join(self.session.work_dir, project_name)
+            if not os.path.exists(project_dir):
+                os.mkdir(project_dir)
+
             archive_name = payload.content_disposition
             if not archive_name:
                 archive_name = 'snapshot.zip'
-            archive_path = os.path.join(self.session.work_dir, archive_name)
+            archive_path = os.path.join(project_dir, archive_name)
 
             archive_file = file(archive_path, 'wb')
             try:
                 shutil.copyfileobj(payload.body, archive_file)
             finally:
                 archive_file.close()
-            basedir = self.unpack_snapshot(msgno, archive_path)
+            basedir = self.unpack_snapshot(msgno, project_dir, archive_name)
 
             try:
-                recipe = Recipe(self.recipe_xml, basedir, self.config)
+                recipe = Recipe(self.build_xml, basedir, self.config)
                 self.execute_build(msgno, recipe)
             finally:
                 if not self.session.keep_files:
                     shutil.rmtree(basedir)
                     os.remove(archive_path)
 
-    def unpack_snapshot(self, msgno, path):
+    def unpack_snapshot(self, msgno, project_dir, archive_name):
         """Unpack a snapshot archive."""
+        path = os.path.join(project_dir, archive_name)
         log.debug('Received snapshot archive: %s', path)
         try:
             zip_file = zipfile.ZipFile(path, 'r')
@@ -142,7 +148,7 @@
                     if name.startswith('/') or '..' in name:
                         continue
                     names.append(os.path.normpath(name))
-                    fullpath = os.path.join(self.session.work_dir, name)
+                    fullpath = os.path.join(project_dir, name)
                     if name.endswith('/'):
                         os.makedirs(fullpath)
                     else:
@@ -157,8 +163,7 @@
             finally:
                 zip_file.close()
 
-            basedir = os.path.join(self.session.work_dir,
-                                   os.path.commonprefix(names))
+            basedir = os.path.join(project_dir,  os.path.commonprefix(names))
             log.debug('Unpacked snapshot to %s' % basedir)
             return basedir
 
--- a/bitten/util/beep.py
+++ b/bitten/util/beep.py
@@ -729,7 +729,7 @@
 
         if data is None:
             data = ''
-        if isinstance(data, xmlio.Element):
+        if isinstance(data, (xmlio.Element, xmlio.ParsedElement)):
             self.body = StringIO(str(data))
         elif isinstance(data, basestring):
             self.body = StringIO(data)
--- a/bitten/util/xmlio.py
+++ b/bitten/util/xmlio.py
@@ -17,6 +17,7 @@
     from cStringIO import StringIO
 except ImportError:
     from StringIO import StringIO
+from UserDict import DictMixin
 
 __all__ = ['Element', 'parse']
 
@@ -185,13 +186,71 @@
 class ParsedElement(object):
     """Representation of an XML element that was parsed from a string or
     file.
+    
+    This class should not be used directly. Rather, XML text parsed using
+    `xmlio.parse()` will return an instance of this class.
+    
+    >>> xml = parse('<root/>')
+    >>> print xml.name
+    root
+    
+    Parsed elements can be serialized to a string using the `write()` method:
+    
+    >>> import sys
+    >>> parse('<root></root>').write(sys.stdout)
+    <root/>
+    
+    For convenience, this is also done when coercing the object to a string
+    using the builtin `str()` function, which is used when `print`ing an object:
+    
+    >>> print parse('<root></root>')
+    <root/>
+    
+    (Note that serializing the element will produce a normalized representation
+    that may not excatly match the input string.)
+    
+    Attributes are accessed via the `attr` member:
+    
+    >>> print parse('<root foo="bar"/>').attr['foo']
+    bar
+    
+    Attributes can also be updated, added or removed:
+    
+    >>> xml = parse('<root foo="bar"/>')
+    >>> xml.attr['foo'] = 'baz'
+    >>> print xml
+    <root foo="baz"/>
+
+    >>> del xml.attr['foo']
+    >>> print xml
+    <root/>
+
+    >>> xml.attr['foo'] = 'bar'
+    >>> print xml
+    <root foo="bar"/>
     """
     __slots__ = ['_node', 'attr']
 
+    class _Attrs(DictMixin):
+        """Simple wrapper around the element attributes to provide a dictionary
+        interface."""
+        def __init__(self, node):
+            self._node = node
+        def __getitem__(self, name):
+            attr = self._node.getAttributeNode(name)
+            if not attr:
+                raise KeyError, name
+            return attr.value.encode()
+        def __setitem__(self, name, value):
+            self._node.setAttribute(name, value)
+        def __delitem__(self, name):
+            self._node.removeAttribute(name)
+        def keys(self):
+            return [name.encode() for name in self._node.attributes.keys()]
+
     def __init__(self, node):
         self._node = node
-        self.attr = dict([(name.encode(), value.encode()) for name, value
-                          in node.attributes.items()])
+        self.attr = ParsedElement._Attrs(node)
 
     name = property(fget=lambda self: self._node.localName)
     namespace = property(fget=lambda self: self._node.namespaceURI)
Copyright (C) 2012-2017 Edgewall Software