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 <Sebastian@SSpaeth.de>
This commit is contained in:
Sebastian Spaeth 2012-01-04 19:24:19 +01:00
parent 8ec6980c96
commit 0ccf06d5e6
4 changed files with 37 additions and 13 deletions

View File

@ -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
---------

View File

@ -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:

View File

@ -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):

View File

@ -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: