Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/changes/maildir.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/changes/maildir.py')
-rw-r--r--buildbot/buildbot/changes/maildir.py116
1 files changed, 116 insertions, 0 deletions
diff --git a/buildbot/buildbot/changes/maildir.py b/buildbot/buildbot/changes/maildir.py
new file mode 100644
index 0000000..2e4a706
--- /dev/null
+++ b/buildbot/buildbot/changes/maildir.py
@@ -0,0 +1,116 @@
+
+# This is a class which watches a maildir for new messages. It uses the
+# linux dirwatcher API (if available) to look for new files. The
+# .messageReceived method is invoked with the filename of the new message,
+# relative to the top of the maildir (so it will look like "new/blahblah").
+
+import os
+from twisted.python import log
+from twisted.application import service, internet
+from twisted.internet import reactor
+dnotify = None
+try:
+ import dnotify
+except:
+ # I'm not actually sure this log message gets recorded
+ log.msg("unable to import dnotify, so Maildir will use polling instead")
+
+class NoSuchMaildir(Exception):
+ pass
+
+class MaildirService(service.MultiService):
+ """I watch a maildir for new messages. I should be placed as the service
+ child of some MultiService instance. When running, I use the linux
+ dirwatcher API (if available) or poll for new files in the 'new'
+ subdirectory of my maildir path. When I discover a new message, I invoke
+ my .messageReceived() method with the short filename of the new message,
+ so the full name of the new file can be obtained with
+ os.path.join(maildir, 'new', filename). messageReceived() should be
+ overridden by a subclass to do something useful. I will not move or
+ delete the file on my own: the subclass's messageReceived() should
+ probably do that.
+ """
+ pollinterval = 10 # only used if we don't have DNotify
+
+ def __init__(self, basedir=None):
+ """Create the Maildir watcher. BASEDIR is the maildir directory (the
+ one which contains new/ and tmp/)
+ """
+ service.MultiService.__init__(self)
+ self.basedir = basedir
+ self.files = []
+ self.dnotify = None
+
+ def setBasedir(self, basedir):
+ # some users of MaildirService (scheduler.Try_Jobdir, in particular)
+ # don't know their basedir until setServiceParent, since it is
+ # relative to the buildmaster's basedir. So let them set it late. We
+ # don't actually need it until our own startService.
+ self.basedir = basedir
+
+ def startService(self):
+ service.MultiService.startService(self)
+ self.newdir = os.path.join(self.basedir, "new")
+ if not os.path.isdir(self.basedir) or not os.path.isdir(self.newdir):
+ raise NoSuchMaildir("invalid maildir '%s'" % self.basedir)
+ try:
+ if dnotify:
+ # we must hold an fd open on the directory, so we can get
+ # notified when it changes.
+ self.dnotify = dnotify.DNotify(self.newdir,
+ self.dnotify_callback,
+ [dnotify.DNotify.DN_CREATE])
+ except (IOError, OverflowError):
+ # IOError is probably linux<2.4.19, which doesn't support
+ # dnotify. OverflowError will occur on some 64-bit machines
+ # because of a python bug
+ log.msg("DNotify failed, falling back to polling")
+ if not self.dnotify:
+ t = internet.TimerService(self.pollinterval, self.poll)
+ t.setServiceParent(self)
+ self.poll()
+
+ def dnotify_callback(self):
+ log.msg("dnotify noticed something, now polling")
+
+ # give it a moment. I found that qmail had problems when the message
+ # was removed from the maildir instantly. It shouldn't, that's what
+ # maildirs are made for. I wasn't able to eyeball any reason for the
+ # problem, and safecat didn't behave the same way, but qmail reports
+ # "Temporary_error_on_maildir_delivery" (qmail-local.c:165,
+ # maildir_child() process exited with rc not in 0,2,3,4). Not sure
+ # why, and I'd have to hack qmail to investigate further, so it's
+ # easier to just wait a second before yanking the message out of new/
+
+ reactor.callLater(0.1, self.poll)
+
+
+ def stopService(self):
+ if self.dnotify:
+ self.dnotify.remove()
+ self.dnotify = None
+ return service.MultiService.stopService(self)
+
+ def poll(self):
+ assert self.basedir
+ # see what's new
+ for f in self.files:
+ if not os.path.isfile(os.path.join(self.newdir, f)):
+ self.files.remove(f)
+ newfiles = []
+ for f in os.listdir(self.newdir):
+ if not f in self.files:
+ newfiles.append(f)
+ self.files.extend(newfiles)
+ # TODO: sort by ctime, then filename, since safecat uses a rather
+ # fine-grained timestamp in the filename
+ for n in newfiles:
+ # TODO: consider catching exceptions in messageReceived
+ self.messageReceived(n)
+
+ def messageReceived(self, filename):
+ """Called when a new file is noticed. Will call
+ self.parent.messageReceived() with a path relative to maildir/new.
+ Should probably be overridden in subclasses."""
+ self.parent.messageReceived(filename)
+