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' EDITOR_APP_UID = 'com.calibre-ebook.edit-book'
MAIN_APP_UID = 'com.calibre-ebook.main-gui' MAIN_APP_UID = 'com.calibre-ebook.main-gui'
STORE_DIALOG_APP_UID = 'com.calibre-ebook.store-dialog' STORE_DIALOG_APP_UID = 'com.calibre-ebook.store-dialog'
TOC_DIALOG_APP_UID = 'com.calibre-ebook.toc-editor'
try: try:
preferred_encoding = locale.getpreferredencoding() preferred_encoding = locale.getpreferredencoding()
codecs.lookup(preferred_encoding) codecs.lookup(preferred_encoding)

View File

@ -6,6 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from collections import OrderedDict from collections import OrderedDict
from PyQt5.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel, 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 import error_dialog, gprefs
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.utils.monotonic import monotonic
from polyglot.builtins import iteritems, unicode_type from polyglot.builtins import iteritems, unicode_type
SUPPORTED = {'EPUB', 'AZW3'} SUPPORTED = {'EPUB', 'AZW3'}
@ -100,6 +102,7 @@ class ToCEditAction(InterfaceAction):
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.edit_books) self.qaction.triggered.connect(self.edit_books)
self.jobs = []
def get_supported_books(self, book_ids): def get_supported_books(self, book_ids):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
@ -138,15 +141,50 @@ class ToCEditAction(InterfaceAction):
self.do_one(book_id, fmt) self.do_one(book_id, fmt)
def do_one(self, book_id, fmt): def do_one(self, book_id, fmt):
from calibre.gui2.toc.main import TOCEditor
db = self.gui.current_db db = self.gui.current_db
path = db.format(book_id, fmt, index_is_id=True, as_path=True) path = db.format(book_id, fmt, index_is_id=True, as_path=True)
title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt
d = TOCEditor(path, title=title, parent=self.gui) data = {'path': path, 'title': title}
d.start() self.gui.job_manager.launch_gui_app('toc-dialog', kwargs=data)
if d.exec_() == d.Accepted: job = data.copy()
with open(path, 'rb') as f: job.update({'book_id': book_id, 'fmt': fmt, 'library_id': db.new_api.library_id, 'started': False, 'start_time': monotonic()})
db.add_format(book_id, fmt, f, index_is_id=True) 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): def edit_books(self):
book_id_map = self.get_books_for_editing() book_id_map = self.get_books_for_editing()

View File

@ -281,9 +281,9 @@ class JobManager(QAbstractTableModel, AdaptSQP): # {{{
self.add_job(job) self.add_job(job)
self.threaded_server.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, 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) self.server.run_job(job, gui=True, redirect_output=False)
def _kill_job(self, job): def _kill_job(self, job):

View File

@ -1,29 +1,37 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai # 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 from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3' import os
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' import sys
__docformat__ = 'restructuredtext en' import textwrap
import sys, os, textwrap
from threading import Thread
from functools import partial from functools import partial
from threading import Thread
from PyQt5.Qt import (QPushButton, QFrame, QMenu, QInputDialog, QCheckBox, from PyQt5.Qt import (
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget, QCheckBox, QCursor, QDialog, QDialogButtonBox, QFrame, QGridLayout, QIcon,
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem, QInputDialog, QItemSelectionModel, QKeySequence, QLabel, QMenu, QPushButton,
QToolButton, QItemSelectionModel, QCursor, QKeySequence, QSizePolicy) 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 ( from calibre.ebooks.oeb.polish.toc import (
get_toc, add_id, TOC, commit_toc, from_xpaths, from_links, from_files) 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 )
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.progress_indicator import ProgressIndicator
from calibre.gui2.toc.location import ItemEdit 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 calibre.utils.logging import GUILog
from polyglot.builtins import map, unicode_type, range from polyglot.builtins import map, range, unicode_type
ICON_SIZE = 24 ICON_SIZE = 24
@ -1117,10 +1125,30 @@ class TOCEditor(QDialog): # {{{
# }}} # }}}
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()
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__': if __name__ == '__main__':
app = Application([], force_calibre_style=True) main(path=sys.argv[-1], title='test')
app os.remove(sys.argv[-1] + '.lock')
d = TOCEditor(sys.argv[-1])
d.start()
d.exec_()
del d # Needed to prevent sigsegv in exit cleanup

View File

@ -88,6 +88,13 @@ def store_dialog(args=sys.argv):
main(args) 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): def gui_ebook_edit(path=None, notify=None):
' For launching the editor from inside calibre ' ' For launching the editor from inside calibre '
init_dbus() init_dbus()

View File

@ -32,6 +32,9 @@ PARALLEL_FUNCS = {
'store-dialog' : 'store-dialog' :
('calibre.gui_launch', 'store_dialog', None), ('calibre.gui_launch', 'store_dialog', None),
'toc-dialog' :
('calibre.gui_launch', 'toc_dialog', None),
'render_pages' : 'render_pages' :
('calibre.ebooks.comic.input', 'render_pages', 'notification'), ('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) 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): class ExclusiveFile(object):
def __init__(self, path, timeout=15, sleep_time=0.2): def __init__(self, path, timeout=15, sleep_time=0.2):
@ -95,17 +108,7 @@ class ExclusiveFile(object):
self.sleep_time = sleep_time self.sleep_time = sleep_time
def __enter__(self): def __enter__(self):
if iswindows: self.file = lock_file(self.path, self.timeout, self.sleep_time)
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
return self.file return self.file
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):