Use a separate process for the ToC Editor from the main GUI

This is needed because the main GUI does not use web engine
This commit is contained in:
Kovid Goyal 2019-06-25 18:41:46 +05:30
parent a304b67ce3
commit 27798beaf6
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 120 additions and 40 deletions

View File

@ -44,6 +44,7 @@ VIEWER_APP_UID = 'com.calibre-ebook.viewer'
EDITOR_APP_UID = 'com.calibre-ebook.edit-book'
MAIN_APP_UID = 'com.calibre-ebook.main-gui'
STORE_DIALOG_APP_UID = 'com.calibre-ebook.store-dialog'
TOC_DIALOG_APP_UID = 'com.calibre-ebook.toc-editor'
try:
preferred_encoding = locale.getpreferredencoding()
codecs.lookup(preferred_encoding)

View File

@ -6,6 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from collections import OrderedDict
from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
@ -13,6 +14,7 @@ from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel,
from calibre.gui2 import error_dialog, gprefs
from calibre.gui2.actions import InterfaceAction
from calibre.utils.monotonic import monotonic
from polyglot.builtins import iteritems, unicode_type
SUPPORTED = {'EPUB', 'AZW3'}
@ -100,6 +102,7 @@ class ToCEditAction(InterfaceAction):
def genesis(self):
self.qaction.triggered.connect(self.edit_books)
self.jobs = []
def get_supported_books(self, book_ids):
db = self.gui.library_view.model().db
@ -138,15 +141,50 @@ class ToCEditAction(InterfaceAction):
self.do_one(book_id, fmt)
def do_one(self, book_id, fmt):
from calibre.gui2.toc.main import TOCEditor
db = self.gui.current_db
path = db.format(book_id, fmt, index_is_id=True, as_path=True)
title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt
d = TOCEditor(path, title=title, parent=self.gui)
d.start()
if d.exec_() == d.Accepted:
with open(path, 'rb') as f:
db.add_format(book_id, fmt, f, index_is_id=True)
data = {'path': path, 'title': title}
self.gui.job_manager.launch_gui_app('toc-dialog', kwargs=data)
job = data.copy()
job.update({'book_id': book_id, 'fmt': fmt, 'library_id': db.new_api.library_id, 'started': False, 'start_time': monotonic()})
self.jobs.append(job)
self.check_for_completions()
def check_for_completions(self):
from calibre.utils.lock import lock_file
for job in tuple(self.jobs):
lock_path = job['path'] + '.lock'
if job['started']:
if not os.path.exists(lock_path):
self.jobs.remove(job)
continue
try:
lf = lock_file(lock_path, timeout=0.01, sleep_time=0.005)
except EnvironmentError:
continue
else:
self.jobs.remove(job)
ret = int(lf.read().decode('ascii'))
lf.close()
os.remove(lock_path)
if ret == 0:
db = self.gui.current_db
if db.new_api.library_id != job['library_id']:
error_dialog(self.gui, _('Library changed'), _(
'Cannot save changes made to {0} by the ToC editor as'
' the calibre library has changed.').format(job['title']), show=True)
else:
db.new_api.add_format(job['book_id'], job['fmt'], job['path'], run_hooks=False)
os.remove(job['path'])
else:
if monotonic() - job['start_time'] > 10:
self.jobs.remove(job)
continue
if os.path.exists(lock_path):
job['started'] = True
if self.jobs:
QTimer.singleShot(100, self.check_for_completions)
def edit_books(self):
book_id_map = self.get_books_for_editing()

View File

@ -281,9 +281,9 @@ class JobManager(QAbstractTableModel, AdaptSQP): # {{{
self.add_job(job)
self.threaded_server.add_job(job)
def launch_gui_app(self, name, args=[], kwargs={}, description=''):
def launch_gui_app(self, name, args=(), kwargs=None, description=''):
job = ParallelJob(name, description, lambda x: x,
args=args, kwargs=kwargs)
args=list(args), kwargs=kwargs or {})
self.server.run_job(job, gui=True, redirect_output=False)
def _kill_job(self, job):

View File

@ -1,29 +1,37 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, textwrap
from threading import Thread
import os
import sys
import textwrap
from functools import partial
from threading import Thread
from PyQt5.Qt import (QPushButton, QFrame, QMenu, QInputDialog, QCheckBox,
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
QToolButton, QItemSelectionModel, QCursor, QKeySequence, QSizePolicy)
from PyQt5.Qt import (
QCheckBox, QCursor, QDialog, QDialogButtonBox, QFrame, QGridLayout, QIcon,
QInputDialog, QItemSelectionModel, QKeySequence, QLabel, QMenu, QPushButton,
QSize, QSizePolicy, QStackedWidget, Qt, QToolButton, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
)
from calibre.ebooks.oeb.polish.container import get_container, AZW3Container
from calibre.constants import TOC_DIALOG_APP_UID, islinux, iswindows
from calibre.ebooks.oeb.polish.container import AZW3Container, get_container
from calibre.ebooks.oeb.polish.toc import (
get_toc, add_id, TOC, commit_toc, from_xpaths, from_links, from_files)
from calibre.gui2 import Application, error_dialog, gprefs, info_dialog, question_dialog
TOC, add_id, commit_toc, from_files, from_links, from_xpaths, get_toc
)
from calibre.gui2 import (
Application, error_dialog, gprefs, info_dialog, question_dialog, set_app_uid
)
from calibre.gui2.convert.xpath_wizard import XPathEdit
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.toc.location import ItemEdit
from calibre.gui2.convert.xpath_wizard import XPathEdit
from calibre.ptempfile import reset_base_dir
from calibre.utils.lock import ExclusiveFile
from calibre.utils.logging import GUILog
from polyglot.builtins import map, unicode_type, range
from polyglot.builtins import map, range, unicode_type
ICON_SIZE = 24
@ -1117,10 +1125,30 @@ class TOCEditor(QDialog): # {{{
# }}}
if __name__ == '__main__':
app = Application([], force_calibre_style=True)
app
d = TOCEditor(sys.argv[-1])
def main(path=None, title=None):
# Ensure we can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
if iswindows:
# Ensure that all instances are grouped together in the task bar. This
# prevents them from being grouped with viewer/editor process when
# launched from within calibre, as both use calibre-parallel.exe
set_app_uid(TOC_DIALOG_APP_UID)
with ExclusiveFile(path + '.lock') as wf:
override = 'calibre-gui' if islinux else None
app = Application([], override_program_name=override)
d = TOCEditor(path, title=title)
d.start()
d.exec_()
del d # Needed to prevent sigsegv in exit cleanup
ret = 1
if d.exec_() == d.Accepted:
ret = 0
wf.write('{}'.format(ret).encode('ascii'))
del d
del app
raise SystemExit(ret)
if __name__ == '__main__':
main(path=sys.argv[-1], title='test')
os.remove(sys.argv[-1] + '.lock')

View File

@ -88,6 +88,13 @@ def store_dialog(args=sys.argv):
main(args)
def toc_dialog(**kw):
detach_gui()
init_dbus()
from calibre.gui2.toc.main import main
main(**kw)
def gui_ebook_edit(path=None, notify=None):
' For launching the editor from inside calibre '
init_dbus()

View File

@ -32,6 +32,9 @@ PARALLEL_FUNCS = {
'store-dialog' :
('calibre.gui_launch', 'store_dialog', None),
'toc-dialog' :
('calibre.gui_launch', 'toc_dialog', None),
'render_pages' :
('calibre.ebooks.comic.input', 'render_pages', 'notification'),

View File

@ -85,6 +85,19 @@ def retry_for_a_time(timeout, sleep_time, func, error_retry, *args):
time.sleep(sleep_time)
def lock_file(path, timeout=15, sleep_time=0.2):
if iswindows:
return retry_for_a_time(
timeout, sleep_time, windows_open, windows_retry, path
)
f = unix_open(path)
retry_for_a_time(
timeout, sleep_time, fcntl.flock, unix_retry,
f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
)
return f
class ExclusiveFile(object):
def __init__(self, path, timeout=15, sleep_time=0.2):
@ -95,17 +108,7 @@ class ExclusiveFile(object):
self.sleep_time = sleep_time
def __enter__(self):
if iswindows:
self.file = retry_for_a_time(
self.timeout, self.sleep_time, windows_open, windows_retry, self.path
)
else:
f = unix_open(self.path)
retry_for_a_time(
self.timeout, self.sleep_time, fcntl.flock, unix_retry,
f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
)
self.file = f
self.file = lock_file(self.path, self.timeout, self.sleep_time)
return self.file
def __exit__(self, type, value, traceback):