Use shared memory for IPC with toc editor

This commit is contained in:
Kovid Goyal 2022-03-22 10:49:29 +05:30
parent 54fd6080fc
commit 3155a36b4b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 62 additions and 40 deletions

View File

@ -6,6 +6,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from itertools import count
from collections import OrderedDict
from qt.core import (
QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QIcon, QLabel, QTimer
@ -100,6 +101,7 @@ class ToCEditAction(InterfaceAction):
self.do_edit(book_id_map)
def genesis(self):
self.shm_count = count()
self.qaction.triggered.connect(self.edit_books)
self.jobs = []
@ -146,34 +148,51 @@ class ToCEditAction(InterfaceAction):
self.do_one(book_id, fmt)
def do_one(self, book_id, fmt):
import struct, json, atexit
from calibre.utils.shm import SharedMemory
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
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()})
job = {'path': path, 'title': title}
data = json.dumps(job).encode('utf-8')
header = struct.pack('>II', 0, 0)
shm = SharedMemory(prefix=f'c{os.getpid()}-{next(self.shm_count)}-', size=len(data) + len(header) + SharedMemory.num_bytes_for_size)
shm.write(header)
shm.write_data_with_size(data)
shm.flush()
atexit.register(shm.close)
self.gui.job_manager.launch_gui_app('toc-dialog', kwargs={'shm_name': shm.name})
job.update({
'book_id': book_id, 'fmt': fmt, 'library_id': db.new_api.library_id, 'shm': shm, 'started': False, 'start_time': monotonic()})
self.jobs.append(job)
self.check_for_completions()
def check_for_completions(self):
from calibre.utils.filenames import retry_on_fail
import struct
def remove_job(job):
job['shm'].close()
self.jobs.remove(job)
for job in tuple(self.jobs):
path = job['path']
started_path = path + '.started'
result_path = path + '.result'
if job['started'] and os.path.exists(result_path):
self.jobs.remove(job)
ret = -1
def read(result_path):
nonlocal ret
with open(result_path) as f:
ret = int(f.read().strip())
retry_on_fail(read, result_path)
retry_on_fail(os.remove, result_path)
if ret == 0:
shm = job['shm']
shm.seek(0)
state, ok = struct.unpack('>II', shm.read(struct.calcsize('>II')))
if state == 0:
# not started
if monotonic() - job['start_time'] > 120:
remove_job(job)
error_dialog(self.gui, _('Failed to start editor'), _(
'Could not edit: {}. The Table of Contents editor did not start in two minutes').format(job['title']), show=True)
elif state == 1:
# running
pass
elif state == 2:
# finished
job['shm'].already_unlinked = True
remove_job(job)
if ok == 1:
db = self.gui.current_db
if db.new_api.library_id != job['library_id']:
error_dialog(self.gui, _('Library changed'), _(
@ -181,16 +200,6 @@ class ToCEditAction(InterfaceAction):
' the calibre library has changed.').format(job['title']), show=True)
else:
db.new_api.add_format(job['book_id'], job['fmt'], path, run_hooks=False)
retry_on_fail(os.remove, path)
elif not job['started']:
if monotonic() - job['start_time'] > 120:
self.jobs.remove(job)
error_dialog(self.gui, _('Failed to start editor'), _(
'Could not edit: {}. The Table of Contents editor did not start in two minutes').format(job['title']), show=True)
continue
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)

View File

@ -1145,7 +1145,11 @@ class TOCEditor(QDialog): # {{{
# }}}
def main(path=None, title=None):
def main(shm_name=None):
import json
import struct
from calibre.utils.shm import SharedMemory
# Ensure we can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
@ -1154,19 +1158,28 @@ def main(path=None, title=None):
# 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 SharedMemory(name=shm_name) as shm:
pos = struct.calcsize('>II')
state, ok = struct.unpack('>II', shm.read(pos))
data = json.loads(shm.read_data_with_size())
title = data['title']
path = data['path']
s = struct.pack('>I', 1)
shm.seek(0), shm.write(s), shm.flush()
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()
ok = 0
if d.exec() == QDialog.DialogCode.Accepted:
ok = 1
s = struct.pack('>II', 2, ok)
shm.seek(0), shm.write(s), shm.flush()
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)
raise SystemExit(0 if ok else 1)
if __name__ == '__main__':