X-Git-Url: https://code.delx.au/offlineimap/blobdiff_plain/39a18fef6078388dac7a1db386cdfd2900f5263c..7bdd4a69fa5621c2fcf3213b32d2c21f1ba2b741:/offlineimap/accounts.py diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index b367de1..55fba9b 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -19,8 +19,72 @@ from offlineimap import threadutil, mbnames, CustomConfig import offlineimap.repository.Base, offlineimap.repository.LocalStatus from offlineimap.ui import UIBase from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread -from threading import Event +from subprocess import Popen, PIPE +from threading import Event, Lock import os +from Queue import Queue, Empty + +class SigListener(Queue): + def __init__(self): + self.folderlock = Lock() + self.folders = None + Queue.__init__(self, 20) + def put_nowait(self, sig): + self.folderlock.acquire() + try: + if sig == 1: + if self.folders is None or not self.autorefreshes: + # folders haven't yet been added, or this account is once-only; drop signal + return + elif self.folders: + for foldernr in range(len(self.folders)): + # requeue folder + self.folders[foldernr][1] = True + self.quick = False + return + # else folders have already been cleared, put signal... + finally: + self.folderlock.release() + Queue.put_nowait(self, sig) + def addfolders(self, remotefolders, autorefreshes, quick): + self.folderlock.acquire() + try: + self.folders = [] + self.quick = quick + self.autorefreshes = autorefreshes + for folder in remotefolders: + # new folders are queued + self.folders.append([folder, True]) + finally: + self.folderlock.release() + def clearfolders(self): + self.folderlock.acquire() + try: + for folder, queued in self.folders: + if queued: + # some folders still in queue + return False + self.folders[:] = [] + return True + finally: + self.folderlock.release() + def queuedfolders(self): + self.folderlock.acquire() + try: + dirty = True + while dirty: + dirty = False + for foldernr, (folder, queued) in enumerate(self.folders): + if queued: + # mark folder as no longer queued + self.folders[foldernr][1] = False + dirty = True + quick = self.quick + self.folderlock.release() + yield (folder, quick) + self.folderlock.acquire() + finally: + self.folderlock.release() def getaccountlist(customconfig): return customconfig.getsectionlist('Account') @@ -45,6 +109,7 @@ class Account(CustomConfig.ConfigHelperMixin): self.localeval = config.getlocaleval() self.ui = UIBase.getglobalui() self.refreshperiod = self.getconffloat('autorefresh', 0.0) + self.quicknum = 0 if self.refreshperiod == 0.0: self.refreshperiod = None @@ -60,7 +125,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. @@ -81,20 +146,25 @@ class Account(CustomConfig.ConfigHelperMixin): item.startkeepalive() refreshperiod = int(self.refreshperiod * 60) - sleepresult = self.ui.sleep(refreshperiod) - if sleepresult == 2: - # Cancel keep-alive, but don't bother terminating threads - for item in kaobjs: - item.stopkeepalive(abrupt = 1) - return sleepresult - else: - # Cancel keep-alive and wait for thread to terminate. - for item in kaobjs: - item.stopkeepalive(abrupt = 0) - return sleepresult +# 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() @@ -110,52 +180,93 @@ 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. + + hook = self.getconf('presynchook', '') + self.callhook(hook) + + quickconfig = self.getconfint('quick', 0) + if quickconfig < 0: + quick = True + elif quickconfig > 0: + if self.quicknum == 0 or self.quicknum > quickconfig: + self.quicknum = 1 + quick = False + else: + self.quicknum = self.quicknum + 1 + quick = True + else: + quick = False + try: remoterepos = self.remoterepos localrepos = self.localrepos statusrepos = self.statusrepos self.ui.syncfolders(remoterepos, localrepos) - remoterepos.syncfoldersto(localrepos) - - 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)) - thread.setDaemon(1) - thread.start() - folderthreads.append(thread) - threadutil.threadsreset(folderthreads) + remoterepos.syncfoldersto(localrepos, [statusrepos]) + + 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() localrepos.holdordropconnections() remoterepos.holdordropconnections() finally: pass + + hook = self.getconf('postsynchook', '') + self.callhook(hook) + + def callhook(self, cmd): + if not cmd: + return + try: + self.ui.callhook("Calling hook: " + cmd) + p = Popen(cmd, shell=True, + stdin=PIPE, stdout=PIPE, stderr=PIPE, + close_fds=True) + r = p.communicate() + self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r) + self.ui.callhook("Hook return code: %d" % p.returncode) + except: + self.ui.warn("Exception occured while calling hook") class SyncableAccount(Account, AccountSynchronizationMixin): pass def syncfolder(accountname, remoterepos, remotefolder, localrepos, - statusrepos): + statusrepos, quick): global mailboxes ui = UIBase.getglobalui() ui.registerthread(accountname) @@ -165,12 +276,6 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, replace(remoterepos.getsep(), localrepos.getsep())) # Write the mailboxes mbnames.add(accountname, localfolder.getvisiblename()) - # Load local folder - ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder) - ui.loadmessagelist(localrepos, localfolder) - localfolder.cachemessagelist() - ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys())) - # Load status folder. statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\ @@ -183,18 +288,31 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, statusfolder.cachemessagelist() + if quick: + if not localfolder.quickchanged(statusfolder) \ + and not remotefolder.quickchanged(statusfolder): + ui.skippingfolder(remotefolder) + localrepos.restore_atime() + return + + # Load local folder + ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder) + ui.loadmessagelist(localrepos, localfolder) + localfolder.cachemessagelist() + ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys())) + # If either the local or the status folder has messages and there is a UID # validity problem, warn and abort. If there are no messages, UW IMAPd # loses UIDVALIDITY. But we don't really need it if both local folders are # empty. So, in that case, just save it off. if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()): if not localfolder.isuidvalidityok(): - ui.validityproblem(localfolder, localfolder.getsaveduidvalidity(), - localfolder.getuidvalidity()) + ui.validityproblem(localfolder) + localrepos.restore_atime() return if not remotefolder.isuidvalidityok(): - ui.validityproblem(remotefolder, remotefolder.getsaveduidvalidity(), - remotefolder.getuidvalidity()) + ui.validityproblem(remotefolder) + localrepos.restore_atime() return else: localfolder.saveuidvalidity() @@ -230,4 +348,5 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder) localfolder.syncmessagesto(statusfolder) statusfolder.save() + localrepos.restore_atime()