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
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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__':
|
||||||
|
@ -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 {{{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user