From 9aeaacfb0110b961aeb87bd0c1d7f91aa13cb001 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Oct 2020 12:45:24 +0530 Subject: [PATCH] Use the new event loop based Listener This means one less thread in the main and viewer UIs and no more polling for messages. --- src/calibre/db/cli/main.py | 13 --- src/calibre/gui2/main.py | 133 ++++++------------------ src/calibre/gui2/store/web_store.py | 27 +++-- src/calibre/gui2/tweak_book/save.py | 9 +- src/calibre/gui2/ui.py | 136 ++++++++++--------------- src/calibre/gui2/viewer/main.py | 153 +++++++++++----------------- src/calibre/gui2/viewer/ui.py | 8 +- 7 files changed, 163 insertions(+), 316 deletions(-) diff --git a/src/calibre/db/cli/main.py b/src/calibre/db/cli/main.py index 1a1788f0e1..914b6aa8a2 100644 --- a/src/calibre/db/cli/main.py +++ b/src/calibre/db/cli/main.py @@ -34,24 +34,11 @@ def option_parser_for(cmd, args=()): return cmd_option_parser -def send_message(msg=''): - prints('Notifying calibre of the change') - from calibre.utils.ipc import RC - t = RC(print_error=False) - t.start() - t.join(3) - if t.done: - t.conn.send('refreshdb:' + msg) - t.conn.close() - - def run_cmd(cmd, opts, args, dbctx): m = module_for_cmd(cmd) if dbctx.is_remote and getattr(m, 'no_remote', False): raise SystemExit(_('The {} command is not supported with remote (server based) libraries').format(cmd)) ret = m.main(opts, args, dbctx) - # if not dbctx.is_remote and not opts.dont_notify_gui and not getattr(m, 'readonly', False): - # send_message() return ret diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index eca93ab5fc..8455de17c3 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -5,7 +5,6 @@ import os import re -import socket import sys import time import traceback @@ -22,10 +21,10 @@ from calibre.gui2 import ( Application, choose_dir, error_dialog, gprefs, initialize_file_icon_provider, question_dialog, setup_gui_option_parser ) +from calibre.gui2.listener import send_message_in_process from calibre.gui2.main_window import option_parser as _option_parser from calibre.gui2.splash_screen import SplashScreen from calibre.utils.config import dynamic, prefs -from calibre.utils.ipc import RC, gui_socket_address from calibre.utils.lock import SingleInstance from calibre.utils.monotonic import monotonic from polyglot.builtins import as_bytes, environ_item, range, unicode_type @@ -211,10 +210,10 @@ class GuiRunner(QObject): '''Make sure an event loop is running before starting the main work of initialization''' - def __init__(self, opts, args, actions, listener, app, gui_debug=None): + def __init__(self, opts, args, actions, app, gui_debug=None): self.startup_time = monotonic() self.timed_print('Starting up...') - self.opts, self.args, self.listener, self.app = opts, args, listener, app + self.opts, self.args, self.app = opts, args, app self.gui_debug = gui_debug self.actions = actions self.main = None @@ -234,7 +233,7 @@ class GuiRunner(QObject): self.splash_screen.show_message(_('Initializing user interface...')) try: with gprefs: # Only write gui.json after initialization is complete - main.initialize(self.library_path, db, self.listener, self.actions) + main.initialize(self.library_path, db, self.actions) finally: self.timed_print('main UI initialized...') if self.splash_screen is not None: @@ -364,8 +363,8 @@ def run_in_debug_mode(): stderr=subprocess.STDOUT, stdin=lopen(os.devnull, 'rb')) -def run_gui(opts, args, listener, app, gui_debug=None): - with listener, SingleInstance('db') as si: +def run_gui(opts, args, app, gui_debug=None): + with SingleInstance('db') as si: if not si: ext = '.exe' if iswindows else '' error_dialog(None, _('Cannot start calibre'), _( @@ -375,10 +374,10 @@ def run_gui(opts, args, listener, app, gui_debug=None): ' program is running, try restarting your computer.').format( 'calibre-server' + ext, 'calibredb' + ext), show=True) return 1 - run_gui_(opts, args, listener, app, gui_debug) + run_gui_(opts, args, app, gui_debug) -def run_gui_(opts, args, listener, app, gui_debug=None): +def run_gui_(opts, args, app, gui_debug=None): initialize_file_icon_provider() app.load_builtin_fonts(scan_for_fonts=True) if not dynamic.get('welcome_wizard_was_run', False): @@ -390,7 +389,7 @@ def run_gui_(opts, args, listener, app, gui_debug=None): actions = tuple(Main.create_application_menubar()) else: actions = tuple(Main.get_menubar_actions()) - runner = GuiRunner(opts, args, actions, listener, app, gui_debug=gui_debug) + runner = GuiRunner(opts, args, actions, app, gui_debug=gui_debug) ret = app.exec_() if getattr(runner.main, 'run_wizard_b4_shutdown', False): from calibre.gui2.wizard import wizard @@ -421,80 +420,42 @@ def run_gui_(opts, args, listener, app, gui_debug=None): singleinstance_name = 'GUI' -def cant_start(msg=_('If you are sure it is not running')+', ', - det_msg=_('Timed out waiting for response from running calibre'), - listener_failed=False): - base = '

%s

%s %s' - where = __appname__ + ' '+_('may be running in the system tray, in the')+' ' - if ismacos: - where += _('upper right region of the screen.') - else: - where += _('lower right region of the screen.') - if iswindows or islinux: - what = _('try rebooting your computer.') - else: - if listener_failed: - path = gui_socket_address() - else: - from calibre.utils.lock import singleinstance_path - path = singleinstance_path(singleinstance_name) - what = _('try deleting the file: "%s"') % path - - info = base%(where, msg, what) - error_dialog(None, _('Cannot start ')+__appname__, - '

'+(_('%s is already running.')%__appname__)+'

'+info, det_msg=det_msg, show=True) - - raise SystemExit(1) +def send_message(msg): + try: + send_message_in_process(msg) + except Exception as err: + print(_('Failed to contact running instance of calibre'), file=sys.stderr, flush=True) + print(err, file=sys.stderr, flush=True) + error_dialog(None, _('Contacting calibre failed'), _( + 'Failed to contact running instance of calibre, try restarting calibre'), + det_msg=str(err), show=True) + return False + return True -def build_pipe(print_error=True): - t = RC(print_error=print_error) - t.start() - t.join(3.0) - if t.is_alive(): - cant_start() - raise SystemExit(1) - return t - - -def shutdown_other(rc=None): - if rc is None: - rc = build_pipe(print_error=False) - if rc.conn is None: - prints(_('No running calibre found')) - return # No running instance found - rc.conn.send(b'shutdown:') - prints(_('Shutdown command sent, waiting for shutdown...')) - for i in range(50): - with SingleInstance(singleinstance_name) as si: - if si: - return - time.sleep(0.1) - prints(_('Failed to shutdown running calibre instance')) - raise SystemExit(1) +def shutdown_other(): + if send_message('shutdown:'): + print(_('Shutdown command sent, waiting for shutdown...'), flush=True) + for i in range(50): + with SingleInstance(singleinstance_name) as si: + if si: + return + time.sleep(0.1) + raise SystemExit(_('Failed to shutdown running calibre instance')) def communicate(opts, args): - t = build_pipe() if opts.shutdown_running_calibre: - shutdown_other(t) + shutdown_other() else: if len(args) > 1: args[1:] = [os.path.abspath(x) if os.path.exists(x) else x for x in args[1:]] import json - t.conn.send(b'launched:'+as_bytes(json.dumps(args))) - t.conn.close() + if not send_message(b'launched:'+as_bytes(json.dumps(args))): + raise SystemExit(_('Failed to contact running instance of calibre')) raise SystemExit(0) -def create_listener(): - if islinux: - from calibre.utils.ipc.server import LinuxListener as Listener - else: - from multiprocessing.connection import Listener - return Listener(address=gui_socket_address()) - - def restart_after_quit(): if iswindows: # detach the stdout/stderr/stdin handles @@ -547,36 +508,8 @@ def main(args=sys.argv): def run_main(app, opts, args, gui_debug, si): if si: - try: - listener = create_listener() - except socket.error: - if iswindows or islinux: - cant_start(det_msg=traceback.format_exc(), listener_failed=True) - if os.path.exists(gui_socket_address()): - os.remove(gui_socket_address()) - try: - listener = create_listener() - except socket.error: - cant_start(det_msg=traceback.format_exc(), listener_failed=True) - else: - return run_gui(opts, args, listener, app, - gui_debug=gui_debug) - else: - return run_gui(opts, args, listener, app, - gui_debug=gui_debug) - otherinstance = False - try: - listener = create_listener() - except socket.error: # Good singleinstance is correct (on UNIX) - otherinstance = True - else: - # On windows only singleinstance can be trusted - otherinstance = True if iswindows else False - if not otherinstance and not opts.shutdown_running_calibre: - return run_gui(opts, args, listener, app, gui_debug=gui_debug) - + return run_gui(opts, args, app, gui_debug=gui_debug) communicate(opts, args) - return 0 diff --git a/src/calibre/gui2/store/web_store.py b/src/calibre/gui2/store/web_store.py index e64be72b21..b158425858 100644 --- a/src/calibre/gui2/store/web_store.py +++ b/src/calibre/gui2/store/web_store.py @@ -6,10 +6,9 @@ import json import os import shutil - from PyQt5.Qt import ( - QHBoxLayout, QIcon, QLabel, QProgressBar, QPushButton, QSize, QUrl, QVBoxLayout, - QWidget, pyqtSignal, QApplication + QApplication, QHBoxLayout, QIcon, QLabel, QProgressBar, QPushButton, QSize, QUrl, + QVBoxLayout, QWidget, pyqtSignal ) from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEngineView @@ -20,11 +19,11 @@ from calibre.gui2 import ( Application, choose_save_file, error_dialog, gprefs, info_dialog, set_app_uid ) from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.gui2.listener import send_message_in_process from calibre.gui2.main_window import MainWindow from calibre.ptempfile import PersistentTemporaryDirectory, reset_base_dir -from calibre.utils.ipc import RC +from polyglot.binary import as_base64_bytes, from_base64_bytes from polyglot.builtins import string_or_bytes -from polyglot.binary import from_base64_bytes, as_base64_bytes class DownloadItem(QWidget): @@ -207,22 +206,20 @@ class Main(MainWindow): shutil.copyfile(path, name) os.remove(path) return - t = RC(print_error=False) - t.start() - t.join(3.0) - if t.conn is None: - error_dialog(self, _('No running calibre'), _( - 'No running calibre instance found. Please start calibre before trying to' - ' download books.'), show=True) - return tags = self.data['tags'] if isinstance(tags, string_or_bytes): tags = list(filter(None, [x.strip() for x in tags.split(',')])) data = json.dumps({'path': path, 'tags': tags}) if not isinstance(data, bytes): data = data.encode('utf-8') - t.conn.send(b'web-store:' + data) - t.conn.close() + + try: + send_message_in_process(b'web-store:' + data) + except Exception as err: + error_dialog(self, _('Could not contact calibre'), _( + 'No running calibre instance found. Please start calibre before trying to' + ' download books.'), det_msg=str(err), show=True) + return info_dialog(self, _('Download completed'), _( 'Download of {0} has been completed, the book was added to' diff --git a/src/calibre/gui2/tweak_book/save.py b/src/calibre/gui2/tweak_book/save.py index 064e0d2fd9..01c7718302 100644 --- a/src/calibre/gui2/tweak_book/save.py +++ b/src/calibre/gui2/tweak_book/save.py @@ -15,7 +15,6 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.progress_indicator import ProgressIndicator from calibre.utils import join_with_timeout from calibre.utils.filenames import atomic_rename, format_permissions -from calibre.utils.ipc import RC from polyglot.queue import LifoQueue, Empty @@ -78,12 +77,8 @@ def save_container(container, path): def send_message(msg=''): if msg: - t = RC(print_error=False) - t.start() - t.join(3) - if t.done: - t.conn.send('bookedited:'+msg) - t.conn.close() + from calibre.gui2.listener import send_message_in_process + send_message_in_process('bookedited:'+msg) def find_first_existing_ancestor(path): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 09cefef239..f591225b09 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -9,83 +9,60 @@ __docformat__ = 'restructuredtext en' '''The main GUI''' -import collections, os, sys, textwrap, time, gc, errno, re -from threading import Thread +import apsw +import collections +import errno +import gc +import os +import re +import sys +import textwrap +import time from collections import OrderedDict from io import BytesIO - -import apsw from PyQt5.Qt import ( - Qt, QTimer, QAction, QMenu, QIcon, pyqtSignal, QUrl, QFont, QDialog, - QApplication, QSystemTrayIcon) + QAction, QApplication, QDialog, QFont, QIcon, QMenu, QSystemTrayIcon, Qt, QTimer, + QUrl, pyqtSignal +) -from calibre import prints, force_unicode, detect_ncpus +from calibre import detect_ncpus, force_unicode, prints from calibre.constants import ( - __appname__, ismacos, iswindows, filesystem_encoding, DEBUG, config_dir) -from calibre.utils.config import prefs, dynamic -from calibre.utils.ipc.pool import Pool + DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows +) +from calibre.customize.ui import available_store_plugins, interface_actions from calibre.db.legacy import LibraryDatabase -from calibre.customize.ui import interface_actions, available_store_plugins -from calibre.gui2 import (error_dialog, GetMetadata, open_url, - gprefs, max_available_height, config, info_dialog, Dispatcher, - question_dialog, warning_dialog) -from calibre.gui2.cover_flow import CoverFlowMixin +from calibre.gui2 import ( + Dispatcher, GetMetadata, config, error_dialog, gprefs, info_dialog, + max_available_height, open_url, question_dialog, warning_dialog +) +from calibre.gui2.auto_add import AutoAdder from calibre.gui2.changes import handle_changes -from calibre.gui2.widgets import ProgressIndicator -from calibre.gui2.update import UpdateMixin -from calibre.gui2.main_window import MainWindow -from calibre.gui2.layout import MainWindowMixin +from calibre.gui2.cover_flow import CoverFlowMixin +from calibre.gui2.dbus_export.widgets import factory from calibre.gui2.device import DeviceMixin -from calibre.gui2.email import EmailMixin +from calibre.gui2.dialogs.message_box import JobError from calibre.gui2.ebook_download import EbookDownloadMixin -from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton -from calibre.gui2.init import LibraryViewMixin, LayoutMixin -from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin +from calibre.gui2.email import EmailMixin +from calibre.gui2.init import LayoutMixin, LibraryViewMixin +from calibre.gui2.job_indicator import Pointer +from calibre.gui2.jobs import JobManager, JobsButton, JobsDialog +from calibre.gui2.keyboard import Manager +from calibre.gui2.layout import MainWindowMixin +from calibre.gui2.listener import Listener +from calibre.gui2.main_window import MainWindow +from calibre.gui2.open_with import register_keyboard_shortcuts +from calibre.gui2.proceed import ProceedQuestion +from calibre.gui2.search_box import SavedSearchBoxMixin, SearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_browser.ui import TagBrowserMixin -from calibre.gui2.keyboard import Manager -from calibre.gui2.auto_add import AutoAdder -from calibre.gui2.proceed import ProceedQuestion -from calibre.gui2.dialogs.message_box import JobError -from calibre.gui2.job_indicator import Pointer -from calibre.gui2.dbus_export.widgets import factory -from calibre.gui2.open_with import register_keyboard_shortcuts +from calibre.gui2.update import UpdateMixin +from calibre.gui2.widgets import ProgressIndicator from calibre.library import current_library_name from calibre.srv.library_broker import GuiLibraryBroker -from polyglot.builtins import unicode_type, string_or_bytes -from polyglot.queue import Queue, Empty - - -class Listener(Thread): # {{{ - - def __init__(self, listener): - Thread.__init__(self) - self.daemon = True - self.listener, self.queue = listener, Queue() - self._run = True - self.start() - - def run(self): - if self.listener is None: - return - while self._run: - try: - conn = self.listener.accept() - msg = conn.recv() - self.queue.put(msg) - except: - continue - - def close(self): - self._run = False - try: - if self.listener is not None: - self.listener.close() - except: - import traceback - traceback.print_exc() - -# }}} +from calibre.utils.config import dynamic, prefs +from calibre.utils.ipc.pool import Pool +from polyglot.builtins import string_or_bytes, unicode_type +from polyglot.queue import Empty, Queue def get_gui(): @@ -93,11 +70,11 @@ def get_gui(): def add_quick_start_guide(library_view, refresh_cover_browser=None): - from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.covers import calibre_cover2 - from calibre.utils.zipfile import safe_replace - from calibre.utils.localization import get_lang, canonicalize_lang + from calibre.ebooks.metadata.meta import get_metadata from calibre.ptempfile import PersistentTemporaryFile + from calibre.utils.localization import canonicalize_lang, get_lang + from calibre.utils.zipfile import safe_replace l = canonicalize_lang(get_lang()) or 'eng' gprefs['quick_start_guide_added'] = True imgbuf = BytesIO(calibre_cover2(_('Quick Start Guide'), '')) @@ -215,7 +192,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ else: stmap[st.name] = st - def initialize(self, library_path, db, listener, actions, show_gui=True): + def initialize(self, library_path, db, actions, show_gui=True): opts = self.opts self.preferences_action, self.quit_action = actions self.library_path = library_path @@ -226,10 +203,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ t.setInterval(1000), t.timeout.connect(self.handle_changes_from_server_debounced), t.setSingleShot(True) self._spare_pool = None self.must_restart_before_config = False - self.listener = Listener(listener) - self.check_messages_timer = QTimer() - self.check_messages_timer.timeout.connect(self.another_instance_wants_to_talk) - self.check_messages_timer.start(1000) for ac in self.iactions.values(): try: @@ -424,6 +397,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.hide_windows() self.auto_adder = AutoAdder(gprefs['auto_add_path'], self) + self.listener = Listener(parent=self) + self.listener.message_received.connect(self.message_from_another_instance) + QTimer.singleShot(0, self.listener.start_listening) + # Collect cycles now gc.collect() @@ -626,11 +603,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if files: self.iactions['Add Books'].add_filesystem_book(files) - def another_instance_wants_to_talk(self): - try: - msg = self.listener.queue.get_nowait() - except Empty: - return + def message_from_another_instance(self, msg): if isinstance(msg, bytes): msg = msg.decode('utf-8', 'replace') if msg.startswith('launched:'): @@ -651,10 +624,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.show_windows() self.raise_() self.activateWindow() - elif msg.startswith('refreshdb:'): - m = self.library_view.model() - m.db.new_api.reload_from_db() - self.refresh_all() elif msg.startswith('shutdown:'): self.quit(confirm_quit=False) elif msg.startswith('bookedited:'): @@ -735,7 +704,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ det_msg=traceback.format_exc() ) if repair: - from calibre.gui2.dialogs.restore_library import repair_library_at + from calibre.gui2.dialogs.restore_library import ( + repair_library_at + ) if repair_library_at(newloc, parent=self): db = LibraryDatabase(newloc, default_prefs=default_prefs) else: @@ -1005,7 +976,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ action.shutting_down() if write_settings: self.write_settings() - self.check_messages_timer.stop() if getattr(self, 'update_checker', None): self.update_checker.shutdown() self.listener.close() diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index fccff954d6..628435cce4 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -6,19 +6,18 @@ import json import os import sys -from threading import Thread - from PyQt5.Qt import QIcon, QObject, Qt, QTimer, pyqtSignal from PyQt5.QtWebEngineCore import QWebEngineUrlScheme +from contextlib import closing -from calibre import as_unicode, prints -from calibre.constants import FAKE_PROTOCOL, VIEWER_APP_UID, islinux, iswindows +from calibre.constants import FAKE_PROTOCOL, VIEWER_APP_UID, islinux from calibre.gui2 import Application, error_dialog, setup_gui_option_parser -from calibre.gui2.viewer.ui import EbookViewer, is_float from calibre.gui2.viewer.config import get_session_pref, vprefs +from calibre.gui2.viewer.ui import EbookViewer, is_float +from calibre.gui2.listener import send_message_in_process from calibre.ptempfile import reset_base_dir from calibre.utils.config import JSONConfig -from calibre.utils.ipc import RC, viewer_socket_address +from calibre.utils.ipc import viewer_socket_address singleinstance_name = 'calibre_viewer' @@ -97,62 +96,16 @@ class EventAccumulator(QObject): self.events = [] -def listen(listener, msg_from_anotherinstance): - while True: +def send_message_to_viewer_instance(args, open_at): + if len(args) > 1: + msg = json.dumps((os.path.abspath(args[1]), open_at)) try: - conn = listener.accept() - except Exception: - break - try: - msg_from_anotherinstance.emit(conn.recv()) - conn.close() - except Exception as e: - prints('Failed to read message from other instance with error: %s' % as_unicode(e)) - - -def create_listener(): - addr = viewer_socket_address() - if islinux: - from calibre.utils.ipc.server import LinuxListener as Listener - else: - from multiprocessing.connection import Listener - if not iswindows: - # On macOS (and BSDs, I am guessing), following a crash, the - # listener socket file sticks around and needs to be explicitly - # removed. It is safe to do this since we are already guaranteed to - # be the owner of the socket by singleinstance() - try: - os.remove(addr) - except Exception: - pass - return Listener(address=addr) - - -def ensure_single_instance(args, open_at): - try: - from calibre.utils.lock import singleinstance - si = singleinstance(singleinstance_name) - except Exception: - import traceback - error_dialog(None, _('Cannot start viewer'), _( - 'Failed to start viewer, could not insure only a single instance of the viewer is running. Click "Show Details" for more information'), - det_msg=traceback.format_exc(), show=True) - raise SystemExit(1) - if not si: - if len(args) > 1: - t = RC(print_error=True, socket_address=viewer_socket_address()) - t.start() - t.join(3.0) - if t.is_alive() or t.conn is None: - error_dialog(None, _('Connect to viewer failed'), _( - 'Unable to connect to existing E-book viewer window, try restarting the viewer.'), show=True) - raise SystemExit(1) - t.conn.send((os.path.abspath(args[1]), open_at)) - t.conn.close() - prints('Opened book in existing viewer instance') - raise SystemExit(0) - listener = create_listener() - return listener + send_message_in_process(msg, address=viewer_socket_address()) + except Exception as err: + error_dialog(None, _('Connect to viewer failed'), _( + 'Unable to connect to existing E-book viewer window, try restarting the viewer.'), det_msg=str(err), show=True) + raise SystemExit(1) + print('Opened book in existing viewer instance') def option_parser(): @@ -187,6 +140,31 @@ View an e-book. return parser +def run_gui(app, opts, args, internal_book_data, listener=None): + acc = EventAccumulator(app) + app.file_event_hook = acc + app.load_builtin_fonts() + app.setWindowIcon(QIcon(I('viewer.png'))) + migrate_previous_viewer_prefs() + main = EbookViewer( + open_at=opts.open_at, continue_reading=opts.continue_reading, force_reload=opts.force_reload, + calibre_book_data=internal_book_data) + main.set_exception_handler() + if len(args) > 1: + acc.events.append(os.path.abspath(args[-1])) + acc.got_file.connect(main.handle_commandline_arg) + main.show() + if listener is not None: + listener.message_received.connect(main.message_from_other_instance, type=Qt.QueuedConnection) + QTimer.singleShot(0, acc.flush) + if opts.raise_window: + main.raise_() + if opts.full_screen: + main.set_full_screen(True) + + app.exec_() + + def main(args=sys.argv): # Ensure viewer can continue to function if GUI is closed os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) @@ -223,42 +201,25 @@ def main(args=sys.argv): oat.startswith('epubcfi(/') or is_float(oat) or oat.startswith('ref:')): raise SystemExit('Not a valid --open-at value: {}'.format(opts.open_at)) - listener = None if get_session_pref('singleinstance', False): - try: - listener = ensure_single_instance(args, opts.open_at) - except Exception as e: - import traceback - error_dialog(None, _('Failed to start viewer'), as_unicode(e), det_msg=traceback.format_exc(), show=True) - raise SystemExit(1) - - acc = EventAccumulator(app) - app.file_event_hook = acc - app.load_builtin_fonts() - app.setWindowIcon(QIcon(I('viewer.png'))) - migrate_previous_viewer_prefs() - main = EbookViewer( - open_at=opts.open_at, continue_reading=opts.continue_reading, force_reload=opts.force_reload, - calibre_book_data=internal_book_data) - main.set_exception_handler() - if len(args) > 1: - acc.events.append(os.path.abspath(args[-1])) - acc.got_file.connect(main.handle_commandline_arg) - main.show() - main.msg_from_anotherinstance.connect(main.another_instance_wants_to_talk, type=Qt.QueuedConnection) - if listener is not None: - t = Thread(name='ConnListener', target=listen, args=(listener, main.msg_from_anotherinstance)) - t.daemon = True - t.start() - QTimer.singleShot(0, acc.flush) - if opts.raise_window: - main.raise_() - if opts.full_screen: - main.set_full_screen(True) - - app.exec_() - if listener is not None: - listener.close() + from calibre.utils.lock import SingleInstance + from calibre.gui2.listener import Listener + with SingleInstance(singleinstance_name) as si: + if si: + try: + listener = Listener(address=viewer_socket_address(), parent=app) + listener.start_listening() + except Exception as err: + error_dialog(None, _('Failed to start listener'), _( + 'Could not start the listener used for single instance viewers. Try rebooting your computer.'), + det_msg=str(err), show=True) + else: + with closing(listener): + run_gui(app, opts, args, internal_book_data, listener=listener) + else: + send_message_to_viewer_instance(args, opts.open_at) + else: + run_gui(app, opts, args, internal_book_data) if __name__ == '__main__': diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index 55b8c0dfd4..8686122098 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -248,13 +248,17 @@ class EbookViewer(MainWindow): else: prints('Cannot read from:', arg, file=sys.stderr) - def another_instance_wants_to_talk(self, msg): + def message_from_other_instance(self, msg): try: + msg = json.loads(msg) path, open_at = msg - except Exception: + except Exception as err: + print('Invalid message from other instance', file=sys.stderr) + print(err, file=sys.stderr) return self.load_ebook(path, open_at=open_at) self.raise_() + self.activateWindow() # }}} # Fullscreen {{{