diff --git a/src/calibre/constants.py b/src/calibre/constants.py index d8bbf2d2c2..e046eb4148 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -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) diff --git a/src/calibre/gui2/actions/toc_edit.py b/src/calibre/gui2/actions/toc_edit.py index c39f3dc2ee..f830245b74 100644 --- a/src/calibre/gui2/actions/toc_edit.py +++ b/src/calibre/gui2/actions/toc_edit.py @@ -6,6 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' __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() diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index 16f138229b..66bf974c94 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -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): diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index f5a00b01ae..b779ec4524 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -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 + from __future__ import absolute_import, division, print_function, unicode_literals -__license__ = 'GPL v3' -__copyright__ = '2013, Kovid Goyal ' -__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): # {{{ # }}} +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__': - app = Application([], force_calibre_style=True) - app - d = TOCEditor(sys.argv[-1]) - d.start() - d.exec_() - del d # Needed to prevent sigsegv in exit cleanup + main(path=sys.argv[-1], title='test') + os.remove(sys.argv[-1] + '.lock') diff --git a/src/calibre/gui_launch.py b/src/calibre/gui_launch.py index f8b35ee2fa..3d8f98303d 100644 --- a/src/calibre/gui_launch.py +++ b/src/calibre/gui_launch.py @@ -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() diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 7c1b1c0f12..f55ac3c951 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -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'), diff --git a/src/calibre/utils/lock.py b/src/calibre/utils/lock.py index 5d1f46820d..fb37f7c8f0 100644 --- a/src/calibre/utils/lock.py +++ b/src/calibre/utils/lock.py @@ -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):