diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 1e7e6f8..4f108f0 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -62,7 +62,7 @@ class Account(CustomConfig.ConfigHelperMixin): def getsection(self): return 'Account ' + self.getname() - def sleeper(self): + def sleeper(self, siglistener): """Sleep handler. Returns same value as UIBase.sleep: 0 if timeout expired, 1 if there was a request to cancel the timer, and 2 if there is a request to abort the program. @@ -83,14 +83,25 @@ class Account(CustomConfig.ConfigHelperMixin): item.startkeepalive() refreshperiod = int(self.refreshperiod * 60) - sleepresult = self.ui.sleep(refreshperiod) +# try: +# sleepresult = siglistener.get_nowait() +# # retrieved signal before sleep started +# if sleepresult == 1: +# # catching signal 1 here means folders were cleared before signal was posted +# pass +# except Empty: +# sleepresult = self.ui.sleep(refreshperiod, siglistener) + sleepresult = self.ui.sleep(refreshperiod, siglistener) + if sleepresult == 1: + self.quicknum = 0 + # Cancel keepalive for item in kaobjs: item.stopkeepalive() return sleepresult class AccountSynchronizationMixin: - def syncrunner(self): + def syncrunner(self, siglistener): self.ui.registerthread(self.name) self.ui.acct(self.name) accountmetadata = self.getaccountmeta() @@ -106,19 +117,19 @@ class AccountSynchronizationMixin: self.statusrepos = offlineimap.repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self) if not self.refreshperiod: - self.sync() + self.sync(siglistener) self.ui.acctdone(self.name) return looping = 1 while looping: - self.sync() - looping = self.sleeper() != 2 + self.sync(siglistener) + looping = self.sleeper(siglistener) != 2 self.ui.acctdone(self.name) def getaccountmeta(self): return os.path.join(self.metadatadir, 'Account-' + self.name) - def sync(self): + def sync(self, siglistener): # We don't need an account lock because syncitall() goes through # each account once, then waits for all to finish. @@ -145,19 +156,24 @@ class AccountSynchronizationMixin: self.ui.syncfolders(remoterepos, localrepos) remoterepos.syncfoldersto(localrepos, [statusrepos]) - folderthreads = [] - for remotefolder in remoterepos.getfolders(): - thread = InstanceLimitedThread(\ - instancename = 'FOLDER_' + self.remoterepos.getname(), - target = syncfolder, - name = "Folder sync %s[%s]" % \ - (self.name, remotefolder.getvisiblename()), - args = (self.name, remoterepos, remotefolder, localrepos, - statusrepos, quick)) - thread.setDaemon(1) - thread.start() - folderthreads.append(thread) - threadutil.threadsreset(folderthreads) + siglistener.addfolders(remoterepos.getfolders(), bool(self.refreshperiod), quick) + + while True: + folderthreads = [] + for remotefolder, quick in siglistener.queuedfolders(): + thread = InstanceLimitedThread(\ + instancename = 'FOLDER_' + self.remoterepos.getname(), + target = syncfolder, + name = "Folder sync %s[%s]" % \ + (self.name, remotefolder.getvisiblename()), + args = (self.name, remoterepos, remotefolder, localrepos, + statusrepos, quick)) + thread.setDaemon(1) + thread.start() + folderthreads.append(thread) + threadutil.threadsreset(folderthreads) + if siglistener.clearfolders(): + break mbnames.write() localrepos.forgetfolders() remoterepos.forgetfolders() diff --git a/offlineimap/init.py b/offlineimap/init.py index 8485511..447a480 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -26,6 +26,7 @@ from offlineimap.CustomConfig import CustomConfigParser from threading import * import threading, socket from getopt import getopt +import signal try: import fcntl @@ -131,6 +132,11 @@ def startup(versionno): lock(config, ui) + def sigterm_handler(signum, frame): + # die immediately + ui.terminate(errormsg="terminating...") + signal.signal(signal.SIGTERM,sigterm_handler) + try: pidfd = open(config.getmetadatadir() + "/pid", "w") pidfd.write(str(os.getpid()) + "\n") @@ -183,12 +189,31 @@ def startup(versionno): else: threadutil.initInstanceLimit(instancename, config.getdefaultint('Repository ' + reposname, "maxconnections", 1)) + siglisteners = [] + def sig_handler(signum, frame): + if signum == signal.SIGUSR1: + # tell each account to do a full sync asap + signum = (1,) + elif signum == signal.SIGHUP: + # tell each account to die asap + signum = (2,) + elif signum == signal.SIGUSR2: + # tell each account to do a full sync asap, then die + signum = (1, 2) + # one listener per account thread (up to maxsyncaccounts) + for listener in siglisteners: + for sig in signum: + listener.put_nowait(sig) + signal.signal(signal.SIGHUP,sig_handler) + signal.signal(signal.SIGUSR1,sig_handler) + signal.signal(signal.SIGUSR2,sig_handler) threadutil.initexitnotify() t = ExitNotifyThread(target=syncmaster.syncitall, name='Sync Runner', kwargs = {'accounts': syncaccounts, - 'config': config}) + 'config': config, + 'siglisteners': siglisteners}) t.setDaemon(1) t.start() except: diff --git a/offlineimap/syncmaster.py b/offlineimap/syncmaster.py index 64f2380..8121416 100644 --- a/offlineimap/syncmaster.py +++ b/offlineimap/syncmaster.py @@ -20,27 +20,31 @@ import imaplib from offlineimap import imapserver, repository, folder, mbnames, threadutil, version from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread import offlineimap.accounts -from offlineimap.accounts import SyncableAccount +from offlineimap.accounts import SyncableAccount, SigListener from offlineimap.ui import UIBase import re, os, os.path, offlineimap, sys from ConfigParser import ConfigParser from threading import * -def syncaccount(threads, config, accountname): +def syncaccount(threads, config, accountname, siglisteners): account = SyncableAccount(config, accountname) + siglistener = SigListener() thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT', target = account.syncrunner, - name = "Account sync %s" % accountname) + name = "Account sync %s" % accountname, + kwargs = {'siglistener': siglistener} ) + # the Sync Runner thread is the only one that will mutate siglisteners + siglisteners.append(siglistener) thread.setDaemon(1) thread.start() threads.add(thread) -def syncitall(accounts, config): +def syncitall(accounts, config, siglisteners): currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE') ui = UIBase.getglobalui() threads = threadutil.threadlist() mbnames.init(config, accounts) for accountname in accounts: - syncaccount(threads, config, accountname) + syncaccount(threads, config, accountname, siglisteners) # Wait for the threads to finish. threads.reset() diff --git a/offlineimap/ui/Blinkenlights.py b/offlineimap/ui/Blinkenlights.py index 577f601..dcc4e01 100644 --- a/offlineimap/ui/Blinkenlights.py +++ b/offlineimap/ui/Blinkenlights.py @@ -132,10 +132,10 @@ class BlinkenBase: s.gettf().setcolor('white') s.__class__.__bases__[-1].callhook(s, msg) - def sleep(s, sleepsecs): + def sleep(s, sleepsecs, siglistener): s.gettf().setcolor('red') s.getaccountframe().startsleep(sleepsecs) - UIBase.sleep(s, sleepsecs) + return UIBase.sleep(s, sleepsecs, siglistener) def sleeping(s, sleepsecs, remainingsecs): if remainingsecs and s.gettf().getcolor() == 'black': diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py index 6c24ab3..8eb709f 100644 --- a/offlineimap/ui/Curses.py +++ b/offlineimap/ui/Curses.py @@ -541,10 +541,10 @@ class Blinkenlights(BlinkenBase, UIBase): s.c.stop() UIBase.mainException(s) - def sleep(s, sleepsecs): + def sleep(s, sleepsecs, siglistener): s.gettf().setcolor('red') s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) - BlinkenBase.sleep(s, sleepsecs) + return BlinkenBase.sleep(s, sleepsecs, siglistener) if __name__ == '__main__': x = Blinkenlights(None) diff --git a/offlineimap/ui/Noninteractive.py b/offlineimap/ui/Noninteractive.py index c86a903..9cd5eca 100644 --- a/offlineimap/ui/Noninteractive.py +++ b/offlineimap/ui/Noninteractive.py @@ -33,10 +33,10 @@ class Basic(UIBase): warntxt = 'warning' sys.stderr.write(warntxt + ": " + str(msg) + "\n") - def sleep(s, sleepsecs): + def sleep(s, sleepsecs, siglistener): if s.verbose >= 0: s._msg("Sleeping for %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) - UIBase.sleep(s, sleepsecs) + return UIBase.sleep(s, sleepsecs, siglistener) def sleeping(s, sleepsecs, remainingsecs): if sleepsecs > 0: diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index ffdbc78..b1176fd 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -19,6 +19,7 @@ import offlineimap.version import re, time, sys, traceback, threading, thread from StringIO import StringIO +from Queue import Empty debugtypes = {'imap': 'IMAP protocol debugging', 'maildir': 'Maildir repository debugging', @@ -330,7 +331,7 @@ class UIBase: ################################################## Other - def sleep(s, sleepsecs): + def sleep(s, sleepsecs, siglistener): """This function does not actually output anything, but handles the overall sleep, dealing with updates as necessary. It will, however, call sleeping() which DOES output something. @@ -340,7 +341,12 @@ class UIBase: abortsleep = 0 while sleepsecs > 0 and not abortsleep: - abortsleep = s.sleeping(1, sleepsecs) + try: + abortsleep = siglistener.get_nowait() + # retrieved signal while sleeping: 1 means immediately resynch, 2 means immediately die + except Empty: + # no signal + abortsleep = s.sleeping(1, sleepsecs) sleepsecs -= 1 s.sleeping(0, 0) # Done sleeping. return abortsleep