[gnome-ostree/wip/autobuilder] autobuilder: New builtin



commit 88237e3f888017c566746d215d9270bcddf0940b
Author: Colin Walters <walters verbum org>
Date:   Wed Oct 17 20:11:07 2012 -0400

    autobuilder: New builtin

 Makefile-ostbuild.am                           |    2 +
 manifest.json                                  |    2 +-
 qa/repoweb/repoweb.js                          |   67 +++-----
 src/ostbuild/pyostbuild/builtin_autobuilder.py |  211 ++++++++++++++++++++++++
 src/ostbuild/pyostbuild/builtin_build.py       |   28 +---
 src/ostbuild/pyostbuild/builtins.py            |    2 +
 src/ostbuild/pyostbuild/filemonitor.py         |   29 +++-
 src/ostbuild/pyostbuild/jsondb.py              |    5 +
 src/ostbuild/pyostbuild/main.py                |    1 +
 src/ostbuild/pyostbuild/mainloop.py            |   26 ++-
 src/ostbuild/pyostbuild/task.py                |  119 +++++++++++++
 11 files changed, 415 insertions(+), 77 deletions(-)
---
diff --git a/Makefile-ostbuild.am b/Makefile-ostbuild.am
index 3169220..2b3b6aa 100644
--- a/Makefile-ostbuild.am
+++ b/Makefile-ostbuild.am
@@ -35,6 +35,7 @@ utilsdir = $(libdir)/ostbuild
 pyostbuilddir=$(libdir)/ostbuild/pyostbuild
 pyostbuild_PYTHON =					\
 	src/ostbuild/pyostbuild/buildutil.py		\
+	src/ostbuild/pyostbuild/builtin_autobuilder.py	\
 	src/ostbuild/pyostbuild/builtin_build.py	\
 	src/ostbuild/pyostbuild/builtin_checkout.py	\
 	src/ostbuild/pyostbuild/builtin_deploy_qemu.py	\
@@ -50,6 +51,7 @@ pyostbuild_PYTHON =					\
 	src/ostbuild/pyostbuild/builtin_source_diff.py	\
 	src/ostbuild/pyostbuild/builtins.py		\
 	src/ostbuild/pyostbuild/filemonitor.py		\
+	src/ostbuild/pyostbuild/task.py		\
 	src/ostbuild/pyostbuild/fileutil.py		\
 	src/ostbuild/pyostbuild/__init__.py		\
 	src/ostbuild/pyostbuild/jsondb.py		\
diff --git a/manifest.json b/manifest.json
index c2c53ae..c6bf224 100644
--- a/manifest.json
+++ b/manifest.json
@@ -627,7 +627,7 @@
 		 "patches": ["libwacom-autogen.patch"]},
 
 		{"src": "linuxwacom:xf86-input-wacom",
-		 "patches": ["libwacom-autogen.patch"]},
+		 "patches": ["xorg-autogen.patch"]},
 
 		{"src": "git:git://github.com/stephenc/tango-icon-naming.git",
 		 "patches": ["tango-icon-naming-python.patch"],
diff --git a/qa/repoweb/repoweb.js b/qa/repoweb/repoweb.js
index ce53059..6c848ae 100644
--- a/qa/repoweb/repoweb.js
+++ b/qa/repoweb/repoweb.js
@@ -7,6 +7,24 @@ function htmlescape(str) {
     return pre.innerHTML.replace(/"/g, "&quot;").replace(/'/g, "&#39;");;
 }
 
+function get_page_arg(key) {
+    var url = window.location.toString();
+    var pos = url.indexOf("?");
+    if (pos == -1)
+        return null;
+
+    var search = url.substr(pos + 1);
+    var params = search.split("&");
+
+    for (var n = 0; n < params.length; n++) {
+        var val = params[n].split("=");
+        if (val[0] == key)
+            return unescape(val[1]);
+    }
+
+    return null;
+}
+
 var repoDataSignal = {};
 var repoData = null;
 
@@ -17,7 +35,8 @@ function repoweb_on_data_loaded(data) {
 }
 
 function repoweb_init() {
-    $.getJSON("data.json", repoweb_on_data_loaded);
+    var id = get_page_arg("prefix");
+    $.getJSON("autobuilder-" + id + ".json", repoweb_on_data_loaded);
 }
 
 function repoweb_index_init() {
@@ -26,44 +45,12 @@ function repoweb_index_init() {
 	$("#repoweb-summary").empty();
 	var summary = $("#repoweb-summary").get(0);
 
-        var load = document.createElement('div');
-        load.appendChild(document.createTextNode('System load: ' + repoData['load']));
-        summary.appendChild(load);
-        
-	var targets = repoData['targets'];
-	for (var name in targets) {
-	    var elt;
-	    var targetData = targets[name];
-	    var div = document.createElement("div");
-	    summary.appendChild(div);
-
-	    elt = document.createElement("h3")
-	    elt.appendChild(document.createTextNode(name));
-	    div.appendChild(elt);
-	    elt = document.createTextNode(targetData['revision']);
-	    div.appendChild(elt);
-	} 
-    });
-}
-
-function repoweb_files_init() {
-    repoweb_init();
-    $(repoDataSignal).on("loaded", function () {
-	$("#repoweb-files").empty();
-	var files = $("#repoweb-files").get(0);
-	var targets = repoData['targets'];
-	for (var name in targets) {
-	    var elt;
-	    var targetData = targets[name];
-	    var div = document.createElement("div");
-	    files.appendChild(div);
-
-	    elt = document.createElement("h3")
-	    elt.appendChild(document.createTextNode(name));
-	    div.appendChild(elt);
-	    elt = document.createElement("pre");
-	    elt.appendChild(document.createTextNode(targetData['files']));
-	    div.appendChild(elt);
-	} 
+        var div = document.createElement('div');
+        div.appendChild(document.createTextNode("Last Build: "));
+        if (repoData['success'])
+            div.appendChild(document.createTextNode("Succeeded"));
+        else
+            div.appendChild(document.createTextNode("Failed"));
+        summary.appendChild(div);
     });
 }
diff --git a/src/ostbuild/pyostbuild/builtin_autobuilder.py b/src/ostbuild/pyostbuild/builtin_autobuilder.py
new file mode 100755
index 0000000..5278c61
--- /dev/null
+++ b/src/ostbuild/pyostbuild/builtin_autobuilder.py
@@ -0,0 +1,211 @@
+# Copyright (C) 2012 Colin Walters <walters verbum org>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import os,sys,subprocess,tempfile,re,shutil
+import argparse
+import time
+import urlparse
+import hashlib
+import json
+from StringIO import StringIO
+
+from . import builtins
+from .ostbuildlog import log, fatal
+from .subprocess_helpers import run_sync, run_sync_get_output
+from .subprocess_helpers import run_sync_monitor_log_file
+from . import ostbuildrc
+from . import buildutil
+from . import mainloop
+from . import fileutil
+from . import kvfile
+from . import filemonitor
+from . import jsondb
+from . import odict
+from . import vcs
+from . import task
+
+class OstbuildAutobuilder(builtins.Builtin):
+    name = "autobuilder"
+    short_description = "Run resolve and build"
+
+    def __init__(self):
+        builtins.Builtin.__init__(self)
+        self.resolve_proc = None
+        self.build_proc = None
+        self.loop = mainloop.Mainloop.get(None)
+        self.prev_source_snapshot_path = None
+        self.source_snapshot_path = None
+        self.build_needed = True
+        self.last_build_succeeded = True
+
+    def _status_is_success(self, estatus):
+        return os.WIFEXITED(estatus) and os.WEXITSTATUS(estatus) == 0
+
+    def _on_resolve_exited(self, pid, status):
+        self.resolve_proc = None
+        success = self._status_is_success(status)
+        self._resolve_taskset.finish(success)
+        log("resolve exited success=%s" % (success, ))
+        self.prev_source_snapshot_path = self.source_snapshot_path
+        self.source_snapshot_path = self.get_src_snapshot_db().get_latest_path()
+        changed = self.prev_source_snapshot_path != self.source_snapshot_path
+        if changed:
+            log("New version is %s" % (self.source_snapshot_path, ))
+        log("scheduling next resolve for %d seconds " % (self.resolve_poll_secs, ))
+        self.loop.timeout_add(self.resolve_poll_secs*1000, self._fetch)
+        if not self.build_needed:
+            self.build_needed = self.prev_source_snapshot_path != self.source_snapshot_path
+        if self.build_needed and self.build_proc is None:
+            self._run_build()
+        else:
+            self._write_status()
+
+    def _fetch(self):
+        self._run_resolve(True)
+        return False
+
+    def _run_resolve(self, fetch=False):
+        assert self.resolve_proc is None
+        workdir = self._resolve_taskset.start()
+        f = open(os.path.join(workdir, 'log'), 'w')
+        args = ['ostbuild', 'resolve', '--manifest=' + self.manifest]
+        if fetch:
+            args.append('--fetch')
+            args.append('--fetch-keep-going')
+        self.resolve_proc = subprocess.Popen(args, stdin=open('/dev/null'), stdout=f, stderr=f)
+        f.close()
+        log("started resolve: pid %d workdir: %s" % (self.resolve_proc.pid, workdir))
+        self.loop.watch_pid(self.resolve_proc.pid, self._on_resolve_exited)
+        self._write_status()
+
+    def _on_build_exited(self, pid, status):
+        self.build_proc = None
+        success = self._status_is_success(status)
+        self._build_taskset.finish(success)
+        log("build exited success=%s" % (success, ))
+        filemonitor.FileMonitor.get().remove(self.build_status_mon_id)
+        self.build_status_mon_id = 0
+        if self.build_needed:
+            self._run_build()
+        else:
+            self._write_status()
+
+    def _on_build_status_changed(self):
+        self._write_status()
+
+    def _write_json_file(self, path, data):
+        path_tmp = path + '.tmp'
+        f = open(path_tmp, 'w')
+        json.dump(data, f, indent=4, sort_keys=True)
+        f.close()
+        os.rename(path_tmp, path)
+
+    def _run_build(self):
+        assert self.build_proc is None
+        assert self.build_needed
+        self.build_needed = False
+        workdir = self._build_taskset.start()
+        statusjson = os.path.join(workdir, 'status.json')
+        f = open(os.path.join(workdir, 'log'), 'w')
+        args = ['ostbuild', 'build', '--skip-vcs-matches',
+                '--src-snapshot=' + self.source_snapshot_path,
+                '--status-json-path=' + statusjson]
+        src_db = self.get_src_snapshot_db()
+        version = src_db.parse_version(os.path.basename(self.source_snapshot_path))
+        meta = {'version': version} 
+        meta_path = os.path.join(workdir, 'meta.json')
+        self._write_json_file(meta_path, meta)
+        self.build_status_json_path = statusjson
+        self.build_status_mon_id = filemonitor.FileMonitor.get().add(self.build_status_json_path,
+                                                                     self._on_build_status_changed)
+        self.build_proc = subprocess.Popen(args, stdin=open('/dev/null'), stdout=f, stderr=f)
+        log("started build: pid %d workdir: %s" % (self.build_proc.pid, workdir))
+        self.loop.watch_pid(self.build_proc.pid, self._on_build_exited)
+        self._write_status()
+
+    def _taskhistory_to_json(self, history):
+        MAXITEMS = 5
+        entries = []
+        for item in history[-MAXITEMS:]:
+            data = {'v': '%d.%d' % (item.major, item.minor),
+                    'state': item.state}
+            entries.append(data)
+            meta_path = os.path.join(item.path, 'meta.json')
+            if os.path.isfile(meta_path):
+                f = open(meta_path)
+                data['meta'] = json.load(f)
+                f.close()
+        return entries
+
+    def _write_status(self):
+        status = {}
+        if self.source_snapshot_path is not None:
+            src_db = self.get_src_snapshot_db()
+            version = src_db.parse_version(os.path.basename(self.source_snapshot_path))
+            status['version'] = version
+        else:
+            status['version'] = ''
+        
+        status['resolve'] = self._taskhistory_to_json(self._resolve_taskset.get_history())
+        build_history = self._build_taskset.get_history()
+        status['build'] = self._taskhistory_to_json(build_history)
+        
+        if self.build_proc is not None:
+            active_build = build_history[-1]
+            status_path = os.path.join(active_build.path, 'status.json')
+            if os.path.isfile(status_path):
+                f = open(status_path)
+                build_status = json.load(f)
+                f.close()
+                status['build-status'] = build_status
+
+        (fd, temppath) = tempfile.mkstemp(suffix='.tmp', prefix='status-',
+                                          dir=os.path.dirname(self.status_path))
+        os.close(fd)
+        f = open(temppath, 'w')
+        json.dump(status, f, indent=4, sort_keys=True)
+        f.close()
+        os.rename(temppath, self.status_path)
+
+    def execute(self, argv):
+        parser = argparse.ArgumentParser(description=self.short_description)
+        parser.add_argument('--prefix')
+        parser.add_argument('--resolve-poll', type=int, default=10*60)
+        parser.add_argument('--manifest', required=True)
+        
+        args = parser.parse_args(argv)
+        self.manifest = args.manifest
+        self.resolve_poll_secs = args.resolve_poll
+        
+        self.parse_config()
+        self.parse_prefix(args.prefix)
+        assert self.prefix is not None
+        self.init_repo()
+        self.source_snapshot_path = self.get_src_snapshot_db().get_latest_path()
+
+        taskdir = task.TaskDir(os.path.join(self.workdir, 'tasks'))
+        self._resolve_taskset = taskdir.get('%s-resolve' % (self.prefix, ))
+        self._build_taskset = taskdir.get('%s-build' % (self.prefix, ))
+
+        self.status_path = os.path.join(self.workdir, 'autobuilder-%s.json' % (self.prefix, ))
+        
+        self._run_resolve()
+        self._run_build()
+
+        self.loop.run()
+
+builtins.register(OstbuildAutobuilder)
diff --git a/src/ostbuild/pyostbuild/builtin_build.py b/src/ostbuild/pyostbuild/builtin_build.py
index 621ce1d..b7d38d5 100755
--- a/src/ostbuild/pyostbuild/builtin_build.py
+++ b/src/ostbuild/pyostbuild/builtin_build.py
@@ -29,6 +29,7 @@ from .subprocess_helpers import run_sync, run_sync_get_output
 from .subprocess_helpers import run_sync_monitor_log_file
 from . import ostbuildrc
 from . import buildutil
+from . import task
 from . import fileutil
 from . import kvfile
 from . import odict
@@ -198,29 +199,6 @@ class OstbuildBuild(builtins.Builtin):
             result.append(csum.hexdigest())
         return result
 
-    def _create_task_workdir(self, taskname):
-        workdir = os.path.join(self.workdir, 'tasks')
-        fileutil.ensure_dir(workdir)
-        serialfile = os.path.join(workdir, 'serial')
-        if not os.path.isfile(serialfile):
-            serial = 0
-        else:
-            f = open(serialfile)
-            serial = int(f.read().strip())
-            f.close()
-
-        serial += 1
-        
-        f = open(serialfile, 'w')
-        f.write('%d\n' % (serial, ))
-        f.close()
-
-        taskdir = os.path.join(workdir, '%s/%d' % (taskname, serial))
-        if os.path.isdir(taskdir):
-            shutil.rmtree(taskdir)
-        fileutil.ensure_dir(taskdir)
-        return taskdir
-
     def _build_one_component(self, component, architecture):
         basename = component['name']
 
@@ -291,7 +269,9 @@ class OstbuildBuild(builtins.Builtin):
             else:
                 log("Need rebuild of %s: %s" % (buildname, rebuild_reason, ) )
 
-        workdir = self._create_task_workdir(buildname)
+        taskdir = task.TaskDir(os.path.join(self.workdir, 'tasks'))
+        build_taskset = taskdir.get(buildname)
+        workdir = build_taskset.start()
 
         temp_metadata_path = os.path.join(workdir, '_ostbuild-meta.json')
         f = open(temp_metadata_path, 'w')
diff --git a/src/ostbuild/pyostbuild/builtins.py b/src/ostbuild/pyostbuild/builtins.py
index cd203bb..ea32006 100755
--- a/src/ostbuild/pyostbuild/builtins.py
+++ b/src/ostbuild/pyostbuild/builtins.py
@@ -178,6 +178,8 @@ class Builtin(object):
     def parse_prefix(self, prefix):
         if prefix is not None:
             self.prefix = prefix
+        else:
+            self.prefix = self.get_prefix()
 
     def parse_snapshot(self, prefix, path):
         self.parse_prefix(prefix)
diff --git a/src/ostbuild/pyostbuild/filemonitor.py b/src/ostbuild/pyostbuild/filemonitor.py
index 0eec07b..c0ce404 100644
--- a/src/ostbuild/pyostbuild/filemonitor.py
+++ b/src/ostbuild/pyostbuild/filemonitor.py
@@ -30,6 +30,7 @@ class FileMonitor(object):
         self._timeout = 1000
         self._timeout_installed = False
         self._loop = mainloop.Mainloop.get(None)
+        self._counter = 0
 
     @classmethod
     def get(cls):
@@ -49,16 +50,38 @@ class FileMonitor(object):
         if path not in self._paths:
             self._paths[path] = []
             self._path_modtimes[path] = self._stat(path)
-        self._paths[path].append(callback)
+        self._counter += 1
+        self._paths[path].append((self._counter, callback))
         if not self._timeout_installed:
             self._timeout_installed = True
             self._loop.timeout_add(self._timeout, self._check_files)
+        return self._counter
+
+    def remove(self, cb_id):
+        found = False
+        for path in self._paths:
+            cbs = self._paths[path]
+            idx = -1
+            for i,(iter_cb_id, callback) in enumerate(cbs):
+                if iter_cb_id == cb_id:
+                    idx = i
+                    break
+            if idx != -1:
+                cbs.pop(idx)
+                found = True
+                break
+        assert found
 
     def _check_files(self):
+        cbs = []
         for (path,callbacks) in self._paths.iteritems():
             mtime = self._stat(path)
             orig_mtime = self._path_modtimes[path]
             if (mtime is not None) and (orig_mtime is None or (mtime > orig_mtime)):
                 self._path_modtimes[path] = mtime
-                for cb in callbacks:
-                    cb()
+                for (counter, cb) in callbacks:
+                    cbs.append(cb)
+        for cb in cbs:
+            cb()
+        self._timeout_installed = len(self._paths) > 0
+        return self._timeout_installed
diff --git a/src/ostbuild/pyostbuild/jsondb.py b/src/ostbuild/pyostbuild/jsondb.py
index 35103b4..44504b0 100644
--- a/src/ostbuild/pyostbuild/jsondb.py
+++ b/src/ostbuild/pyostbuild/jsondb.py
@@ -70,6 +70,11 @@ class JsonDB(object):
             return None
         return os.path.join(self._dirpath, files[0][3])
 
+    def parse_version(self, name):
+        match = self._version_csum_re.search(name)
+        assert match is not None
+        return '%s.%s' % (match.group(1), match.group(2))
+
     def store(self, obj):
         files = self._get_all()
         if len(files) == 0:
diff --git a/src/ostbuild/pyostbuild/main.py b/src/ostbuild/pyostbuild/main.py
index 7676f26..1da65da 100755
--- a/src/ostbuild/pyostbuild/main.py
+++ b/src/ostbuild/pyostbuild/main.py
@@ -22,6 +22,7 @@ import sys
 import argparse
 
 from . import builtins
+from . import builtin_autobuilder
 from . import builtin_build
 from . import builtin_checkout
 from . import builtin_deploy_root
diff --git a/src/ostbuild/pyostbuild/mainloop.py b/src/ostbuild/pyostbuild/mainloop.py
index 40a67be..ec34799 100644
--- a/src/ostbuild/pyostbuild/mainloop.py
+++ b/src/ostbuild/pyostbuild/mainloop.py
@@ -29,6 +29,7 @@ class Mainloop(object):
         self._timeouts = []
         self._pid_watches = {}
         self._fd_callbacks = {}
+        self._source_counter = 0
 
     @classmethod
     def get(cls, context):
@@ -55,7 +56,9 @@ class Mainloop(object):
         self._pid_watches[pid] = callback
 
     def timeout_add(self, ms, callback):
-        self._timeouts.append((ms, callback))
+        self._source_counter += 1
+        self._timeouts.append((self._source_counter, ms, callback))
+        return self._source_counter
 
     def quit(self):
         self._running = False
@@ -64,7 +67,7 @@ class Mainloop(object):
         min_timeout = None
         if len(self._pid_watches) > 0:
             min_timeout = 500
-        for (ms, callback) in self._timeouts:
+        for (source_id, ms, callback) in self._timeouts:
             if (min_timeout is None) or (ms < min_timeout):
                 min_timeout = ms
         origtime = time.time() * 1000
@@ -72,23 +75,28 @@ class Mainloop(object):
         fds = self.poll.poll(min_timeout)
         for fd in fds:
             self._fd_callbacks[fd]()
-        to_delete_pids = []
+        hit_pids = []
         for pid in self._pid_watches:
             (opid, status) = os.waitpid(pid, os.WNOHANG)
             if opid == pid:
-                to_delete_pids.append(pid)
-                self._pid_watches[pid](pid, status)
-        for pid in to_delete_pids:
+                hit_pids.append(pid)
+        for pid in hit_pids:
+            watch = self._pid_watches[pid]
             del self._pid_watches[pid]
+            watch(pid, status)
         newtime = time.time() * 1000
         diff = int(newtime - origtime)
         if diff < 0: diff = 0
-        for i,(ms, callback) in enumerate(self._timeouts):
+        new_timeouts = []
+        for i,(source_id, ms, callback) in enumerate(self._timeouts):
             remaining_ms = ms - diff
             if remaining_ms <= 0:
-                callback()
+                result = callback()
+                if result:
+                    new_timeouts.append((source_id, ms, callback))
             else:
-                self._timeouts[i] = (remaining_ms, callback)
+                new_timeouts.append((source_id, remaining_ms, callback))
+        self._timeouts = new_timeouts
 
     def run(self):
         self._running = True
diff --git a/src/ostbuild/pyostbuild/task.py b/src/ostbuild/pyostbuild/task.py
new file mode 100644
index 0000000..7ed224e
--- /dev/null
+++ b/src/ostbuild/pyostbuild/task.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2011 Colin Walters <walters verbum org>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import os,sys,subprocess,tempfile,re,shutil
+import argparse
+import time
+import urlparse
+import hashlib
+import json
+
+from . import fileutil
+
+VERSION_RE = re.compile(r'(\d+)\.(\d+)')
+
+class TaskDir(object):
+    def __init__(self, path):
+        self.path = path
+
+    def get(self, name):
+        task_path = os.path.join(self.path, name)
+        fileutil.ensure_dir(task_path)
+
+        return TaskSet(task_path)
+        
+class TaskHistoryEntry(object):
+    def __init__(self, path, state=None):
+        self.path = path
+        match = VERSION_RE.match(os.path.basename(path))
+        assert match is not None
+        self.major = int(match.group(1))
+        self.minor = int(match.group(2))
+        if state is None:
+            statuspath = os.path.join(self.path, 'status')
+            if os.path.isfile(statuspath):
+                f = open(statuspath)
+                self.state = f.read()
+                f.close()
+            else:
+                self.state = 'interrupted'
+        else:
+            self.state = state
+
+    def finish(self, success):
+        statuspath = os.path.join(self.path, 'status')
+        f = open(statuspath, 'w')
+        if success:
+            success_str = 'success'
+        else:
+            success_str = 'failed'
+        self.state = success_str
+        f.write(success_str)
+        f.close()
+
+    def __cmp__(self, other):
+        if not isinstance(other, TaskHistoryEntry):
+            return -1
+        elif (self.major != other.major):
+            return cmp(self.major, other.major)
+        else:
+            return cmp(self.minor, other.minor)
+
+class TaskSet(object):
+    def __init__(self, path):
+        self.path = path
+
+        self._history = []
+        self._running = False
+        self._running_version = None
+
+        self._load()
+
+    def _load(self):
+        for item in os.listdir(self.path):
+            match = VERSION_RE.match(item)
+            if match is None:
+                continue
+            history_path = os.path.join(self.path, item)
+            self._history.append(TaskHistoryEntry(history_path))
+        self._history.sort()
+
+    def start(self):
+        assert not self._running
+        self._running = True
+        yearver = time.gmtime().tm_year
+        if len(self._history) == 0:
+            lastversion = -1 
+        else:
+            last = self._history[-1]
+            if last.major == yearver:
+                lastversion = last.minor
+            else:
+                lastversion = -1 
+        history_path = os.path.join(self.path, '%d.%d' % (yearver, lastversion + 1))
+        fileutil.ensure_dir(history_path)
+        self._history.append(TaskHistoryEntry(history_path, state='running'))
+        return history_path
+
+    def finish(self, success):
+        assert self._running
+        last = self._history[-1]
+        last.finish(success)
+        self._running = False
+
+    def get_history(self):
+        return self._history



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]