Linux/OSX: Shutdown gracefully on receiving interrupt or terminate signals

This commit is contained in:
Kovid Goyal 2016-08-03 11:14:39 +05:30
parent 57efd828ef
commit ece360f04c
8 changed files with 39 additions and 8 deletions

View File

@ -1,13 +1,13 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """ """ The GUI """
import os, sys, Queue, threading, glob import os, sys, Queue, threading, glob, signal
from contextlib import contextmanager from contextlib import contextmanager
from threading import RLock, Lock from threading import RLock, Lock
from urllib import unquote from urllib import unquote
from PyQt5.QtWidgets import QStyle # Gives a nicer error message than import from Qt from PyQt5.QtWidgets import QStyle # Gives a nicer error message than import from Qt
from PyQt5.Qt import ( from PyQt5.Qt import (
QFileInfo, QObject, QBuffer, Qt, QByteArray, QTranslator, QFileInfo, QObject, QBuffer, Qt, QByteArray, QTranslator, QSocketNotifier,
QCoreApplication, QThread, QEvent, QTimer, pyqtSignal, QDateTime, QCoreApplication, QThread, QEvent, QTimer, pyqtSignal, QDateTime,
QDesktopServices, QFileDialog, QFileIconProvider, QSettings, QIcon, QDesktopServices, QFileDialog, QFileIconProvider, QSettings, QIcon,
QApplication, QDialog, QUrl, QFont, QFontDatabase, QLocale, QFontInfo) QApplication, QDialog, QUrl, QFont, QFontDatabase, QLocale, QFontInfo)
@ -860,6 +860,8 @@ def setup_gui_option_parser(parser):
class Application(QApplication): class Application(QApplication):
shutdown_signal_received = pyqtSignal()
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs): def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs):
self.file_event_hook = None self.file_event_hook = None
if override_program_name: if override_program_name:
@ -872,6 +874,8 @@ class Application(QApplication):
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
self.pi = plugins['progress_indicator'][0] self.pi = plugins['progress_indicator'][0]
QApplication.__init__(self, qargs) QApplication.__init__(self, qargs)
if not iswindows:
self.setup_unix_signals()
if islinux or isbsd: if islinux or isbsd:
self.setAttribute(Qt.AA_DontUseNativeMenuBar, 'CALIBRE_NO_NATIVE_MENUBAR' in os.environ) self.setAttribute(Qt.AA_DontUseNativeMenuBar, 'CALIBRE_NO_NATIVE_MENUBAR' in os.environ)
self.setup_styles(force_calibre_style) self.setup_styles(force_calibre_style)
@ -1048,6 +1052,28 @@ class Application(QApplication):
def __exit__(self, *args): def __exit__(self, *args):
self.setQuitOnLastWindowClosed(True) self.setQuitOnLastWindowClosed(True)
def setup_unix_signals(self):
import fcntl
read_fd, write_fd = os.pipe()
cloexec_flag = getattr(fcntl, 'FD_CLOEXEC', 1)
for fd in (read_fd, write_fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags | cloexec_flag | os.O_NONBLOCK)
for sig in (signal.SIGINT, signal.SIGTERM):
signal.signal(sig, lambda x, y: None)
signal.siginterrupt(sig, False)
signal.set_wakeup_fd(write_fd)
self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self)
self.signal_notifier.setEnabled(True)
self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection)
def signal_received(self, read_fd):
try:
os.read(read_fd, 1024)
except EnvironmentError:
return
self.shutdown_signal_received.emit()
_store_app = None _store_app = None
@contextmanager @contextmanager

View File

@ -86,7 +86,6 @@ class MainWindow(QMainWindow):
___menu = None ___menu = None
__actions = [] __actions = []
keyboard_interrupt = pyqtSignal()
# See https://bugreports.qt-project.org/browse/QTBUG-42281 # See https://bugreports.qt-project.org/browse/QTBUG-42281
window_blocked = pyqtSignal() window_blocked = pyqtSignal()
window_unblocked = pyqtSignal() window_unblocked = pyqtSignal()
@ -131,8 +130,7 @@ class MainWindow(QMainWindow):
sys.excepthook = ExceptionHandler(self) sys.excepthook = ExceptionHandler(self)
def unhandled_exception(self, type, value, tb): def unhandled_exception(self, type, value, tb):
if type == KeyboardInterrupt: if type is KeyboardInterrupt:
self.keyboard_interrupt.emit()
return return
try: try:
sio = StringIO.StringIO() sio = StringIO.StringIO()

View File

@ -178,7 +178,7 @@ def main(control_conn, data_conn):
while True: while True:
try: try:
request = eintr_retry_call(control_conn.recv) request = eintr_retry_call(control_conn.recv)
except EOFError: except (KeyboardInterrupt, EOFError):
break break
if request is None: if request is None:
break break

View File

@ -71,6 +71,7 @@ def _run(args, notify=None):
main = Main(opts, notify=notify) main = Main(opts, notify=notify)
main.set_exception_handler() main.set_exception_handler()
main.show() main.show()
app.shutdown_signal_received.connect(main.boss.quit)
if len(args) > 1: if len(args) > 1:
main.boss.open_book(args[1], edit_file=args[2:], clear_notify_data=False) main.boss.open_book(args[1], edit_file=args[2:], clear_notify_data=False)
else: else:

View File

@ -397,8 +397,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
self.start_content_server() self.start_content_server()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings() self.read_settings()
self.finalize_layout() self.finalize_layout()
if self.bars_manager.showing_donate: if self.bars_manager.showing_donate:
@ -425,6 +423,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# Collect cycles now # Collect cycles now
gc.collect() gc.collect()
QApplication.instance().shutdown_signal_received.connect(self.quit)
if show_gui and self.gui_debug is not None: if show_gui and self.gui_debug is not None:
QTimer.singleShot(10, self.show_gui_debug_msg) QTimer.singleShot(10, self.show_gui_debug_msg)
@ -843,6 +842,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def quit(self, checked=True, restart=False, debug_on_restart=False, def quit(self, checked=True, restart=False, debug_on_restart=False,
confirm_quit=True): confirm_quit=True):
if self.shutting_down:
return
if confirm_quit and not self.confirm_quit(): if confirm_quit and not self.confirm_quit():
return return
try: try:

View File

@ -178,6 +178,7 @@ class EbookViewer(MainWindow):
self.action_reload = QAction(_('&Reload book'), self) self.action_reload = QAction(_('&Reload book'), self)
self.action_reload.triggered.connect(self.reload_book) self.action_reload.triggered.connect(self.reload_book)
self.action_quit.triggered.connect(self.quit) self.action_quit.triggered.connect(self.quit)
QApplication.instance().shutdown_signal_received.connect(self.action_quit.trigger)
self.action_reference_mode.triggered[bool].connect(self.view.reference_mode) self.action_reference_mode.triggered[bool].connect(self.view.reference_mode)
self.action_metadata.triggered[bool].connect(self.metadata.setVisible) self.action_metadata.triggered[bool].connect(self.metadata.setVisible)
self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible) self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible)

View File

@ -337,6 +337,8 @@ def worker_main(conn):
job = cPickle.loads(eintr_retry_call(conn.recv_bytes)) job = cPickle.loads(eintr_retry_call(conn.recv_bytes))
except EOFError: except EOFError:
break break
except KeyboardInterrupt:
break
except Exception: except Exception:
prints('recv() failed in worker, terminating worker', file=sys.stderr) prints('recv() failed in worker, terminating worker', file=sys.stderr)
import traceback import traceback

View File

@ -70,6 +70,8 @@ class OffloadWorker(object):
def shutdown(self): def shutdown(self):
try: try:
eintr_retry_call(self.conn.send, None) eintr_retry_call(self.conn.send, None)
except IOError:
pass
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()