mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
cb88451596
commit
9aeaacfb01
@ -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
|
||||
|
||||
|
||||
|
@ -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 = '<p>%s</p><p>%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__,
|
||||
'<p>'+(_('%s is already running.')%__appname__)+'</p>'+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
|
||||
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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__':
|
||||
|
@ -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 {{{
|
||||
|
Loading…
x
Reference in New Issue
Block a user