From ece360f04cfcbf1679eeb5d05e270f9ebb56ac75 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Aug 2016 11:14:39 +0530 Subject: [PATCH] Linux/OSX: Shutdown gracefully on receiving interrupt or terminate signals --- src/calibre/gui2/__init__.py | 30 +++++++++++++++++-- src/calibre/gui2/main_window.py | 4 +-- .../gui2/tweak_book/completion/worker.py | 2 +- src/calibre/gui2/tweak_book/main.py | 1 + src/calibre/gui2/ui.py | 5 ++-- src/calibre/gui2/viewer/main.py | 1 + src/calibre/utils/ipc/pool.py | 2 ++ src/calibre/utils/ipc/simple_worker.py | 2 ++ 8 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 6a85560e90..217876994b 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1,13 +1,13 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' """ The GUI """ -import os, sys, Queue, threading, glob +import os, sys, Queue, threading, glob, signal from contextlib import contextmanager from threading import RLock, Lock from urllib import unquote from PyQt5.QtWidgets import QStyle # Gives a nicer error message than import from Qt from PyQt5.Qt import ( - QFileInfo, QObject, QBuffer, Qt, QByteArray, QTranslator, + QFileInfo, QObject, QBuffer, Qt, QByteArray, QTranslator, QSocketNotifier, QCoreApplication, QThread, QEvent, QTimer, pyqtSignal, QDateTime, QDesktopServices, QFileDialog, QFileIconProvider, QSettings, QIcon, QApplication, QDialog, QUrl, QFont, QFontDatabase, QLocale, QFontInfo) @@ -860,6 +860,8 @@ def setup_gui_option_parser(parser): class Application(QApplication): + shutdown_signal_received = pyqtSignal() + def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs): self.file_event_hook = None 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] self.pi = plugins['progress_indicator'][0] QApplication.__init__(self, qargs) + if not iswindows: + self.setup_unix_signals() if islinux or isbsd: self.setAttribute(Qt.AA_DontUseNativeMenuBar, 'CALIBRE_NO_NATIVE_MENUBAR' in os.environ) self.setup_styles(force_calibre_style) @@ -1048,6 +1052,28 @@ class Application(QApplication): def __exit__(self, *args): 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 @contextmanager diff --git a/src/calibre/gui2/main_window.py b/src/calibre/gui2/main_window.py index eccc0be6d4..ee05563e7d 100644 --- a/src/calibre/gui2/main_window.py +++ b/src/calibre/gui2/main_window.py @@ -86,7 +86,6 @@ class MainWindow(QMainWindow): ___menu = None __actions = [] - keyboard_interrupt = pyqtSignal() # See https://bugreports.qt-project.org/browse/QTBUG-42281 window_blocked = pyqtSignal() window_unblocked = pyqtSignal() @@ -131,8 +130,7 @@ class MainWindow(QMainWindow): sys.excepthook = ExceptionHandler(self) def unhandled_exception(self, type, value, tb): - if type == KeyboardInterrupt: - self.keyboard_interrupt.emit() + if type is KeyboardInterrupt: return try: sio = StringIO.StringIO() diff --git a/src/calibre/gui2/tweak_book/completion/worker.py b/src/calibre/gui2/tweak_book/completion/worker.py index cb8cba21a9..18e2a14ff6 100644 --- a/src/calibre/gui2/tweak_book/completion/worker.py +++ b/src/calibre/gui2/tweak_book/completion/worker.py @@ -178,7 +178,7 @@ def main(control_conn, data_conn): while True: try: request = eintr_retry_call(control_conn.recv) - except EOFError: + except (KeyboardInterrupt, EOFError): break if request is None: break diff --git a/src/calibre/gui2/tweak_book/main.py b/src/calibre/gui2/tweak_book/main.py index 59f5603af6..fed5d43e20 100644 --- a/src/calibre/gui2/tweak_book/main.py +++ b/src/calibre/gui2/tweak_book/main.py @@ -71,6 +71,7 @@ def _run(args, notify=None): main = Main(opts, notify=notify) main.set_exception_handler() main.show() + app.shutdown_signal_received.connect(main.boss.quit) if len(args) > 1: main.boss.open_book(args[1], edit_file=args[2:], clear_notify_data=False) else: diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 7b42fd3a14..8b0bb758fa 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -397,8 +397,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if config['autolaunch_server']: self.start_content_server() - self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) - self.read_settings() self.finalize_layout() if self.bars_manager.showing_donate: @@ -425,6 +423,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ # Collect cycles now gc.collect() + QApplication.instance().shutdown_signal_received.connect(self.quit) if show_gui and self.gui_debug is not None: 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, confirm_quit=True): + if self.shutting_down: + return if confirm_quit and not self.confirm_quit(): return try: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index d9756934cf..7ebc95fd45 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -178,6 +178,7 @@ class EbookViewer(MainWindow): self.action_reload = QAction(_('&Reload book'), self) self.action_reload.triggered.connect(self.reload_book) 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_metadata.triggered[bool].connect(self.metadata.setVisible) self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible) diff --git a/src/calibre/utils/ipc/pool.py b/src/calibre/utils/ipc/pool.py index f37104c2ee..dbd8bde274 100644 --- a/src/calibre/utils/ipc/pool.py +++ b/src/calibre/utils/ipc/pool.py @@ -337,6 +337,8 @@ def worker_main(conn): job = cPickle.loads(eintr_retry_call(conn.recv_bytes)) except EOFError: break + except KeyboardInterrupt: + break except Exception: prints('recv() failed in worker, terminating worker', file=sys.stderr) import traceback diff --git a/src/calibre/utils/ipc/simple_worker.py b/src/calibre/utils/ipc/simple_worker.py index 72cb23fcbb..c57b72f010 100644 --- a/src/calibre/utils/ipc/simple_worker.py +++ b/src/calibre/utils/ipc/simple_worker.py @@ -70,6 +70,8 @@ class OffloadWorker(object): def shutdown(self): try: eintr_retry_call(self.conn.send, None) + except IOError: + pass except: import traceback traceback.print_exc()