From e1a3458c51b16c2dd1e4314e0dab6a87f65b2c39 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 25 May 2021 22:11:19 +0530 Subject: [PATCH] Make communication with toc editor process more robust A crash on dialog close no longer affects writing the result. And avoid using the racy lock files for ipc. --- src/calibre/gui2/actions/toc_edit.py | 44 ++++++++++++---------------- src/calibre/gui2/toc/main.py | 35 ++++++++++++++-------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/calibre/gui2/actions/toc_edit.py b/src/calibre/gui2/actions/toc_edit.py index 6da9d49e6c..bbd5719840 100644 --- a/src/calibre/gui2/actions/toc_edit.py +++ b/src/calibre/gui2/actions/toc_edit.py @@ -152,37 +152,31 @@ class ToCEditAction(InterfaceAction): self.check_for_completions() def check_for_completions(self): - from calibre.utils.lock import lock_file + from calibre.utils.filenames import retry_on_fail 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']) + started_path = job['path'] + '.started' + result_path = job['path'] + '.result' + if job['started'] and os.path.exists(result_path): + self.jobs.remove(job) + with open(result_path) as f: + ret = int(f.read().strip()) + retry_on_fail(os.remove, result_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): + if os.path.exists(started_path): job['started'] = True + retry_on_fail(os.remove, started_path) if self.jobs: QTimer.singleShot(100, self.check_for_completions) diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index 05612cd339..4d48640a0c 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -5,6 +5,7 @@ import os import sys +import tempfile import textwrap from functools import partial from qt.core import ( @@ -29,7 +30,7 @@ from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.toc.location import ItemEdit from calibre.ptempfile import reset_base_dir from calibre.utils.config import JSONConfig -from calibre.utils.lock import ExclusiveFile +from calibre.utils.filenames import atomic_rename from calibre.utils.logging import GUILog from polyglot.builtins import map, range, unicode_type @@ -985,8 +986,9 @@ class TOCEditor(QDialog): # {{{ explode_done = pyqtSignal(object) writing_done = pyqtSignal(object) - def __init__(self, pathtobook, title=None, parent=None, prefs=None): + def __init__(self, pathtobook, title=None, parent=None, prefs=None, write_result_to=None): QDialog.__init__(self, parent) + self.write_result_to = write_result_to self.prefs = prefs or te_prefs self.pathtobook = pathtobook self.working = True @@ -1071,7 +1073,7 @@ class TOCEditor(QDialog): # {{{ ' more information.')%self.book_title, det_msg=tb, show=True) super(TOCEditor, self).reject() return - + self.write_result(0) super(TOCEditor, self).accept() def reject(self): @@ -1083,8 +1085,17 @@ class TOCEditor(QDialog): # {{{ else: self.working = False self.prefs['toc_editor_window_geom'] = bytearray(self.saveGeometry()) + self.write_result(1) super(TOCEditor, self).reject() + def write_result(self, res): + if self.write_result_to: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(self.write_result_to), delete=False) as f: + src = f.name + f.write(str(res).encode('utf-8')) + f.flush() + atomic_rename(src, self.write_result_to) + def start(self): t = Thread(target=self.explode) t.daemon = True @@ -1140,15 +1151,15 @@ def main(path=None, title=None): # 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_() == QDialog.DialogCode.Accepted: - ret = 0 - wf.write('{}'.format(ret).encode('ascii')) + with open(path + '.started', 'w'): + pass + override = 'calibre-gui' if islinux else None + app = Application([], override_program_name=override) + d = TOCEditor(path, title=title, write_result_to=path + '.result') + d.start() + ret = 1 + if d.exec_() == QDialog.DialogCode.Accepted: + ret = 0 del d del app raise SystemExit(ret)