From 0ccf06d5e631db2a5e139d63335134d983591487 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Wed, 4 Jan 2012 19:24:19 +0100 Subject: [PATCH] Implement clean CTRL-C termination Previously, we would simply bail out in an ugly way, potentially leaving temporary files around etc, or while writing status files. Hand SIGINT and SIGTERM as an event to the Account class, and make that bail out cleanly at predefined points. Stopping on ctrl-c can take a few seconds (it will e.g. finish to transfer the ongoing message), but it will shut down cleanly. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 4 ++++ offlineimap/accounts.py | 23 ++++++++++++++++++----- offlineimap/folder/Base.py | 7 +++++++ offlineimap/init.py | 16 ++++++++-------- 4 files changed, 37 insertions(+), 13 deletions(-) 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: