diff --git a/Changelog.draft.rst b/Changelog.draft.rst index a9b7de4..8b9b8bd 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -29,5 +29,9 @@ Changes * Bumped bundled imaplib2 to release 2.29 +* Make ctrl-c exit cleanly rather aborting brutally (which could leave + around temporary files, half-written cache files, etc). Exiting on + SIGTERM and CTRL-C can take a little longer, but will be clean. + Bug Fixes --------- diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 6c1d7f2..857fbfb 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -50,7 +50,9 @@ class Account(CustomConfig.ConfigHelperMixin): :class:`accounts.SyncableAccount` which contains all functions used for syncing an account.""" #signal gets set when we should stop looping - abort_signal = Event() + abort_soon_signal = Event() + #signal gets set on CTRL-C/SIGTERM + abort_NOW_signal = Event() def __init__(self, config, name): """ @@ -97,7 +99,8 @@ class Account(CustomConfig.ConfigHelperMixin): set_abort_event() to send the corresponding signal. Signum = 1 implies that we want all accounts to abort or skip the current or next sleep phase. Signum = 2 will end the autorefresh loop, - ie all accounts will return after they finished a sync. + ie all accounts will return after they finished a sync. signum=3 + means, abort NOW, e.g. on SIGINT or SIGTERM. This is a class method, it will send the signal to all accounts. """ @@ -107,7 +110,10 @@ class Account(CustomConfig.ConfigHelperMixin): config.set('Account ' + acctsection, "skipsleep", '1') elif signum == 2: # don't autorefresh anymore - cls.abort_signal.set() + cls.abort_soon_signal.set() + elif signum == 3: + # abort ASAP + cls.abort_NOW_signal.set() def get_abort_event(self): """Checks if an abort signal had been sent @@ -122,7 +128,8 @@ class Account(CustomConfig.ConfigHelperMixin): skipsleep = self.getconfboolean("skipsleep", 0) if skipsleep: self.config.set(self.getsection(), "skipsleep", '0') - return skipsleep or Account.abort_signal.is_set() + return skipsleep or Account.abort_soon_signal.is_set() or \ + Account.abort_NOW_signal.is_set() def sleeper(self): """Sleep if the account is set to autorefresh @@ -152,7 +159,8 @@ class Account(CustomConfig.ConfigHelperMixin): item.stopkeepalive() if sleepresult: - if Account.abort_signal.is_set(): + if Account.abort_soon_signal.is_set() or \ + Account.abort_NOW_signal.is_set(): return 2 self.quicknum = 0 return 1 @@ -288,6 +296,8 @@ class SyncableAccount(Account): # iterate through all folders on the remote repo and sync for remotefolder in remoterepos.getfolders(): + # check for CTRL-C or SIGTERM + if Account.abort_NOW_signal.is_set(): break if not remotefolder.sync_this: self.ui.debug('', "Not syncing filtered remote folder '%s'" "[%s]" % (remotefolder, remoterepos)) @@ -320,6 +330,9 @@ class SyncableAccount(Account): self.callhook(hook) def callhook(self, cmd): + # check for CTRL-C or SIGTERM and run postsynchook + if Account.abort_NOW_signal.is_set(): + return if not cmd: return try: diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 53bc71c..f61e89d 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -18,6 +18,7 @@ from offlineimap import threadutil from offlineimap.ui import getglobalui from offlineimap.error import OfflineImapError +import offlineimap.accounts import os.path import re from sys import exc_info @@ -332,6 +333,9 @@ class BaseFolder(object): self.getmessageuidlist()) num_to_copy = len(copylist) for num, uid in enumerate(copylist): + # bail out on CTRL-C or SIGTERM + if offlineimap.accounts.Account.abort_NOW_signal.is_set(): + break self.ui.copyingmessage(uid, num+1, num_to_copy, self, dstfolder) # exceptions are caught in copymessageto() if self.suggeststhreads(): @@ -447,6 +451,9 @@ class BaseFolder(object): ('syncing flags' , self.syncmessagesto_flags)] for (passdesc, action) in passes: + # bail out on CTRL-C or SIGTERM + if offlineimap.accounts.Account.abort_NOW_signal.is_set(): + break try: action(dstfolder, statusfolder) except (KeyboardInterrupt): diff --git a/offlineimap/init.py b/offlineimap/init.py index 36240ac..4287b5e 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -288,11 +288,6 @@ class OfflineImap: self.config is supposed to have been correctly initialized already.""" - def sigterm_handler(signum, frame): - # die immediately - self.ui.terminate(errormsg="terminating...") - signal.signal(signal.SIGTERM, sigterm_handler) - try: pidfd = open(self.config.getmetadatadir() + "/pid", "w") pidfd.write(str(os.getpid()) + "\n") @@ -328,11 +323,19 @@ class OfflineImap: accounts.Account.set_abort_event(self.config, 1) elif sig == signal.SIGUSR2: # tell each account to stop looping + getglobalui().warn("Terminating after this sync...") accounts.Account.set_abort_event(self.config, 2) + elif sig == signal.SIGTERM or sig == signal.SIGINT: + # tell each account to ABORT ASAP (ctrl-c) + getglobalui().warn("Terminating NOW (this may "\ + "take a few seconds)...") + accounts.Account.set_abort_event(self.config, 3) signal.signal(signal.SIGHUP,sig_handler) signal.signal(signal.SIGUSR1,sig_handler) signal.signal(signal.SIGUSR2,sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + signal.signal(signal.SIGINT, sig_handler) #various initializations that need to be performed: offlineimap.mbnames.init(self.config, syncaccounts) @@ -361,9 +364,6 @@ class OfflineImap: t.start() threadutil.exitnotifymonitorloop(threadutil.threadexited) self.ui.terminate() - except KeyboardInterrupt: - self.ui.terminate(1, errormsg = 'CTRL-C pressed, aborting...') - return except (SystemExit): raise except Exception, e: