Use the new event loop based Listener

This means one less thread in the main and viewer UIs and no more polling for
messages.
This commit is contained in:
Kovid Goyal 2020-10-20 12:45:24 +05:30
parent cb88451596
commit 9aeaacfb01
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 163 additions and 316 deletions

View File

@ -34,24 +34,11 @@ def option_parser_for(cmd, args=()):
return cmd_option_parser 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): def run_cmd(cmd, opts, args, dbctx):
m = module_for_cmd(cmd) m = module_for_cmd(cmd)
if dbctx.is_remote and getattr(m, 'no_remote', False): if dbctx.is_remote and getattr(m, 'no_remote', False):
raise SystemExit(_('The {} command is not supported with remote (server based) libraries').format(cmd)) raise SystemExit(_('The {} command is not supported with remote (server based) libraries').format(cmd))
ret = m.main(opts, args, dbctx) 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 return ret

View File

@ -5,7 +5,6 @@
import os import os
import re import re
import socket
import sys import sys
import time import time
import traceback import traceback
@ -22,10 +21,10 @@ from calibre.gui2 import (
Application, choose_dir, error_dialog, gprefs, initialize_file_icon_provider, Application, choose_dir, error_dialog, gprefs, initialize_file_icon_provider,
question_dialog, setup_gui_option_parser 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.main_window import option_parser as _option_parser
from calibre.gui2.splash_screen import SplashScreen from calibre.gui2.splash_screen import SplashScreen
from calibre.utils.config import dynamic, prefs 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.lock import SingleInstance
from calibre.utils.monotonic import monotonic from calibre.utils.monotonic import monotonic
from polyglot.builtins import as_bytes, environ_item, range, unicode_type 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 '''Make sure an event loop is running before starting the main work of
initialization''' 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.startup_time = monotonic()
self.timed_print('Starting up...') 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.gui_debug = gui_debug
self.actions = actions self.actions = actions
self.main = None self.main = None
@ -234,7 +233,7 @@ class GuiRunner(QObject):
self.splash_screen.show_message(_('Initializing user interface...')) self.splash_screen.show_message(_('Initializing user interface...'))
try: try:
with gprefs: # Only write gui.json after initialization is complete 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: finally:
self.timed_print('main UI initialized...') self.timed_print('main UI initialized...')
if self.splash_screen is not None: if self.splash_screen is not None:
@ -364,8 +363,8 @@ def run_in_debug_mode():
stderr=subprocess.STDOUT, stdin=lopen(os.devnull, 'rb')) stderr=subprocess.STDOUT, stdin=lopen(os.devnull, 'rb'))
def run_gui(opts, args, listener, app, gui_debug=None): def run_gui(opts, args, app, gui_debug=None):
with listener, SingleInstance('db') as si: with SingleInstance('db') as si:
if not si: if not si:
ext = '.exe' if iswindows else '' ext = '.exe' if iswindows else ''
error_dialog(None, _('Cannot start calibre'), _( 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( ' program is running, try restarting your computer.').format(
'calibre-server' + ext, 'calibredb' + ext), show=True) 'calibre-server' + ext, 'calibredb' + ext), show=True)
return 1 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() initialize_file_icon_provider()
app.load_builtin_fonts(scan_for_fonts=True) app.load_builtin_fonts(scan_for_fonts=True)
if not dynamic.get('welcome_wizard_was_run', False): 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()) actions = tuple(Main.create_application_menubar())
else: else:
actions = tuple(Main.get_menubar_actions()) 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_() ret = app.exec_()
if getattr(runner.main, 'run_wizard_b4_shutdown', False): if getattr(runner.main, 'run_wizard_b4_shutdown', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
@ -421,80 +420,42 @@ def run_gui_(opts, args, listener, app, gui_debug=None):
singleinstance_name = 'GUI' singleinstance_name = 'GUI'
def cant_start(msg=_('If you are sure it is not running')+', ', def send_message(msg):
det_msg=_('Timed out waiting for response from running calibre'), try:
listener_failed=False): send_message_in_process(msg)
base = '<p>%s</p><p>%s %s' except Exception as err:
where = __appname__ + ' '+_('may be running in the system tray, in the')+' ' print(_('Failed to contact running instance of calibre'), file=sys.stderr, flush=True)
if ismacos: print(err, file=sys.stderr, flush=True)
where += _('upper right region of the screen.') error_dialog(None, _('Contacting calibre failed'), _(
else: 'Failed to contact running instance of calibre, try restarting calibre'),
where += _('lower right region of the screen.') det_msg=str(err), show=True)
if iswindows or islinux: return False
what = _('try rebooting your computer.') return True
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__,
'<p>'+(_('%s is already running.')%__appname__)+'</p>'+info, det_msg=det_msg, show=True)
raise SystemExit(1)
def build_pipe(print_error=True): def shutdown_other():
t = RC(print_error=print_error) if send_message('shutdown:'):
t.start() print(_('Shutdown command sent, waiting for shutdown...'), flush=True)
t.join(3.0) for i in range(50):
if t.is_alive(): with SingleInstance(singleinstance_name) as si:
cant_start() if si:
raise SystemExit(1) return
return t time.sleep(0.1)
raise SystemExit(_('Failed to shutdown running calibre instance'))
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 communicate(opts, args): def communicate(opts, args):
t = build_pipe()
if opts.shutdown_running_calibre: if opts.shutdown_running_calibre:
shutdown_other(t) shutdown_other()
else: else:
if len(args) > 1: if len(args) > 1:
args[1:] = [os.path.abspath(x) if os.path.exists(x) else x for x in args[1:]] args[1:] = [os.path.abspath(x) if os.path.exists(x) else x for x in args[1:]]
import json import json
t.conn.send(b'launched:'+as_bytes(json.dumps(args))) if not send_message(b'launched:'+as_bytes(json.dumps(args))):
t.conn.close() raise SystemExit(_('Failed to contact running instance of calibre'))
raise SystemExit(0) 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(): def restart_after_quit():
if iswindows: if iswindows:
# detach the stdout/stderr/stdin handles # detach the stdout/stderr/stdin handles
@ -547,36 +508,8 @@ def main(args=sys.argv):
def run_main(app, opts, args, gui_debug, si): def run_main(app, opts, args, gui_debug, si):
if si: if si:
try: return run_gui(opts, args, app, gui_debug=gui_debug)
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)
communicate(opts, args) communicate(opts, args)
return 0 return 0

View File

@ -6,10 +6,9 @@
import json import json
import os import os
import shutil import shutil
from PyQt5.Qt import ( from PyQt5.Qt import (
QHBoxLayout, QIcon, QLabel, QProgressBar, QPushButton, QSize, QUrl, QVBoxLayout, QApplication, QHBoxLayout, QIcon, QLabel, QProgressBar, QPushButton, QSize, QUrl,
QWidget, pyqtSignal, QApplication QVBoxLayout, QWidget, pyqtSignal
) )
from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEngineView 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 Application, choose_save_file, error_dialog, gprefs, info_dialog, set_app_uid
) )
from calibre.gui2.dialogs.confirm_delete import confirm 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.gui2.main_window import MainWindow
from calibre.ptempfile import PersistentTemporaryDirectory, reset_base_dir 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.builtins import string_or_bytes
from polyglot.binary import from_base64_bytes, as_base64_bytes
class DownloadItem(QWidget): class DownloadItem(QWidget):
@ -207,22 +206,20 @@ class Main(MainWindow):
shutil.copyfile(path, name) shutil.copyfile(path, name)
os.remove(path) os.remove(path)
return 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'] tags = self.data['tags']
if isinstance(tags, string_or_bytes): if isinstance(tags, string_or_bytes):
tags = list(filter(None, [x.strip() for x in tags.split(',')])) tags = list(filter(None, [x.strip() for x in tags.split(',')]))
data = json.dumps({'path': path, 'tags': tags}) data = json.dumps({'path': path, 'tags': tags})
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = data.encode('utf-8') 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'), _( info_dialog(self, _('Download completed'), _(
'Download of {0} has been completed, the book was added to' 'Download of {0} has been completed, the book was added to'

View File

@ -15,7 +15,6 @@ from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils import join_with_timeout from calibre.utils import join_with_timeout
from calibre.utils.filenames import atomic_rename, format_permissions from calibre.utils.filenames import atomic_rename, format_permissions
from calibre.utils.ipc import RC
from polyglot.queue import LifoQueue, Empty from polyglot.queue import LifoQueue, Empty
@ -78,12 +77,8 @@ def save_container(container, path):
def send_message(msg=''): def send_message(msg=''):
if msg: if msg:
t = RC(print_error=False) from calibre.gui2.listener import send_message_in_process
t.start() send_message_in_process('bookedited:'+msg)
t.join(3)
if t.done:
t.conn.send('bookedited:'+msg)
t.conn.close()
def find_first_existing_ancestor(path): def find_first_existing_ancestor(path):

View File

@ -9,83 +9,60 @@ __docformat__ = 'restructuredtext en'
'''The main GUI''' '''The main GUI'''
import collections, os, sys, textwrap, time, gc, errno, re import apsw
from threading import Thread import collections
import errno
import gc
import os
import re
import sys
import textwrap
import time
from collections import OrderedDict from collections import OrderedDict
from io import BytesIO from io import BytesIO
import apsw
from PyQt5.Qt import ( from PyQt5.Qt import (
Qt, QTimer, QAction, QMenu, QIcon, pyqtSignal, QUrl, QFont, QDialog, QAction, QApplication, QDialog, QFont, QIcon, QMenu, QSystemTrayIcon, Qt, QTimer,
QApplication, QSystemTrayIcon) QUrl, pyqtSignal
)
from calibre import prints, force_unicode, detect_ncpus from calibre import detect_ncpus, force_unicode, prints
from calibre.constants import ( from calibre.constants import (
__appname__, ismacos, iswindows, filesystem_encoding, DEBUG, config_dir) DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows
from calibre.utils.config import prefs, dynamic )
from calibre.utils.ipc.pool import Pool from calibre.customize.ui import available_store_plugins, interface_actions
from calibre.db.legacy import LibraryDatabase from calibre.db.legacy import LibraryDatabase
from calibre.customize.ui import interface_actions, available_store_plugins from calibre.gui2 import (
from calibre.gui2 import (error_dialog, GetMetadata, open_url, Dispatcher, GetMetadata, config, error_dialog, gprefs, info_dialog,
gprefs, max_available_height, config, info_dialog, Dispatcher, max_available_height, open_url, question_dialog, warning_dialog
question_dialog, warning_dialog) )
from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.auto_add import AutoAdder
from calibre.gui2.changes import handle_changes from calibre.gui2.changes import handle_changes
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.update import UpdateMixin from calibre.gui2.dbus_export.widgets import factory
from calibre.gui2.main_window import MainWindow
from calibre.gui2.layout import MainWindowMixin
from calibre.gui2.device import DeviceMixin 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.ebook_download import EbookDownloadMixin
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton from calibre.gui2.email import EmailMixin
from calibre.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.init import LayoutMixin, LibraryViewMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin 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.search_restriction_mixin import SearchRestrictionMixin
from calibre.gui2.tag_browser.ui import TagBrowserMixin from calibre.gui2.tag_browser.ui import TagBrowserMixin
from calibre.gui2.keyboard import Manager from calibre.gui2.update import UpdateMixin
from calibre.gui2.auto_add import AutoAdder from calibre.gui2.widgets import ProgressIndicator
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.library import current_library_name from calibre.library import current_library_name
from calibre.srv.library_broker import GuiLibraryBroker from calibre.srv.library_broker import GuiLibraryBroker
from polyglot.builtins import unicode_type, string_or_bytes from calibre.utils.config import dynamic, prefs
from polyglot.queue import Queue, Empty from calibre.utils.ipc.pool import Pool
from polyglot.builtins import string_or_bytes, unicode_type
from polyglot.queue import Empty, Queue
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()
# }}}
def get_gui(): def get_gui():
@ -93,11 +70,11 @@ def get_gui():
def add_quick_start_guide(library_view, refresh_cover_browser=None): 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.ebooks.covers import calibre_cover2
from calibre.utils.zipfile import safe_replace from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.localization import get_lang, canonicalize_lang
from calibre.ptempfile import PersistentTemporaryFile 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' l = canonicalize_lang(get_lang()) or 'eng'
gprefs['quick_start_guide_added'] = True gprefs['quick_start_guide_added'] = True
imgbuf = BytesIO(calibre_cover2(_('Quick Start Guide'), '')) imgbuf = BytesIO(calibre_cover2(_('Quick Start Guide'), ''))
@ -215,7 +192,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
else: else:
stmap[st.name] = st 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 opts = self.opts
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
self.library_path = library_path 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) t.setInterval(1000), t.timeout.connect(self.handle_changes_from_server_debounced), t.setSingleShot(True)
self._spare_pool = None self._spare_pool = None
self.must_restart_before_config = False 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(): for ac in self.iactions.values():
try: try:
@ -424,6 +397,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.hide_windows() self.hide_windows()
self.auto_adder = AutoAdder(gprefs['auto_add_path'], self) 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 # Collect cycles now
gc.collect() gc.collect()
@ -626,11 +603,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if files: if files:
self.iactions['Add Books'].add_filesystem_book(files) self.iactions['Add Books'].add_filesystem_book(files)
def another_instance_wants_to_talk(self): def message_from_another_instance(self, msg):
try:
msg = self.listener.queue.get_nowait()
except Empty:
return
if isinstance(msg, bytes): if isinstance(msg, bytes):
msg = msg.decode('utf-8', 'replace') msg = msg.decode('utf-8', 'replace')
if msg.startswith('launched:'): if msg.startswith('launched:'):
@ -651,10 +624,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.show_windows() self.show_windows()
self.raise_() self.raise_()
self.activateWindow() 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:'): elif msg.startswith('shutdown:'):
self.quit(confirm_quit=False) self.quit(confirm_quit=False)
elif msg.startswith('bookedited:'): elif msg.startswith('bookedited:'):
@ -735,7 +704,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
det_msg=traceback.format_exc() det_msg=traceback.format_exc()
) )
if repair: 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): if repair_library_at(newloc, parent=self):
db = LibraryDatabase(newloc, default_prefs=default_prefs) db = LibraryDatabase(newloc, default_prefs=default_prefs)
else: else:
@ -1005,7 +976,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
action.shutting_down() action.shutting_down()
if write_settings: if write_settings:
self.write_settings() self.write_settings()
self.check_messages_timer.stop()
if getattr(self, 'update_checker', None): if getattr(self, 'update_checker', None):
self.update_checker.shutdown() self.update_checker.shutdown()
self.listener.close() self.listener.close()

View File

@ -6,19 +6,18 @@
import json import json
import os import os
import sys import sys
from threading import Thread
from PyQt5.Qt import QIcon, QObject, Qt, QTimer, pyqtSignal from PyQt5.Qt import QIcon, QObject, Qt, QTimer, pyqtSignal
from PyQt5.QtWebEngineCore import QWebEngineUrlScheme 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
from calibre.constants import FAKE_PROTOCOL, VIEWER_APP_UID, islinux, iswindows
from calibre.gui2 import Application, error_dialog, setup_gui_option_parser 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.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.ptempfile import reset_base_dir
from calibre.utils.config import JSONConfig 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' singleinstance_name = 'calibre_viewer'
@ -97,62 +96,16 @@ class EventAccumulator(QObject):
self.events = [] self.events = []
def listen(listener, msg_from_anotherinstance): def send_message_to_viewer_instance(args, open_at):
while True: if len(args) > 1:
msg = json.dumps((os.path.abspath(args[1]), open_at))
try: try:
conn = listener.accept() send_message_in_process(msg, address=viewer_socket_address())
except Exception: except Exception as err:
break error_dialog(None, _('Connect to viewer failed'), _(
try: 'Unable to connect to existing E-book viewer window, try restarting the viewer.'), det_msg=str(err), show=True)
msg_from_anotherinstance.emit(conn.recv()) raise SystemExit(1)
conn.close() print('Opened book in existing viewer instance')
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
def option_parser(): def option_parser():
@ -187,6 +140,31 @@ View an e-book.
return parser 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): def main(args=sys.argv):
# Ensure viewer can continue to function if GUI is closed # Ensure viewer can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) 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:')): oat.startswith('epubcfi(/') or is_float(oat) or oat.startswith('ref:')):
raise SystemExit('Not a valid --open-at value: {}'.format(opts.open_at)) raise SystemExit('Not a valid --open-at value: {}'.format(opts.open_at))
listener = None
if get_session_pref('singleinstance', False): if get_session_pref('singleinstance', False):
try: from calibre.utils.lock import SingleInstance
listener = ensure_single_instance(args, opts.open_at) from calibre.gui2.listener import Listener
except Exception as e: with SingleInstance(singleinstance_name) as si:
import traceback if si:
error_dialog(None, _('Failed to start viewer'), as_unicode(e), det_msg=traceback.format_exc(), show=True) try:
raise SystemExit(1) listener = Listener(address=viewer_socket_address(), parent=app)
listener.start_listening()
acc = EventAccumulator(app) except Exception as err:
app.file_event_hook = acc error_dialog(None, _('Failed to start listener'), _(
app.load_builtin_fonts() 'Could not start the listener used for single instance viewers. Try rebooting your computer.'),
app.setWindowIcon(QIcon(I('viewer.png'))) det_msg=str(err), show=True)
migrate_previous_viewer_prefs() else:
main = EbookViewer( with closing(listener):
open_at=opts.open_at, continue_reading=opts.continue_reading, force_reload=opts.force_reload, run_gui(app, opts, args, internal_book_data, listener=listener)
calibre_book_data=internal_book_data) else:
main.set_exception_handler() send_message_to_viewer_instance(args, opts.open_at)
if len(args) > 1: else:
acc.events.append(os.path.abspath(args[-1])) run_gui(app, opts, args, internal_book_data)
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()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -248,13 +248,17 @@ class EbookViewer(MainWindow):
else: else:
prints('Cannot read from:', arg, file=sys.stderr) prints('Cannot read from:', arg, file=sys.stderr)
def another_instance_wants_to_talk(self, msg): def message_from_other_instance(self, msg):
try: try:
msg = json.loads(msg)
path, open_at = 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 return
self.load_ebook(path, open_at=open_at) self.load_ebook(path, open_at=open_at)
self.raise_() self.raise_()
self.activateWindow()
# }}} # }}}
# Fullscreen {{{ # Fullscreen {{{