Skip to content

Commit 0318219

Browse files
committed
pdf-converter: stop DispVM conversion when GUI cancel is clicked
Cancel in the Zenity progress dialog did not stop the running conversion job.\nThe pipeline wrapper could exit while qvm-convert-pdf (and qrexec subprocesses)\nkept running in the background.\n\nFix this in two places:\n- qvm-convert-pdf.gnome now tracks converter PID and sends SIGTERM when\n Zenity returns non-zero (Cancel).\n- client cleanup now terminates an active qrexec process on CancelledError,\n handles SIGTERM the same way as SIGINT, and fixes the termination guard\n to stop running processes on failures.\n\nAlso add async unit tests for cancellation and signal registration paths.\n\nFixes QubesOS/qubes-issues#10274
1 parent ddd0d1f commit 0318219

3 files changed

Lines changed: 100 additions & 6 deletions

File tree

qubespdfconverter/client.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -491,11 +491,13 @@ async def run(self, archive, depth, in_place):
491491

492492
self.bar.set_job_status(Status.FAIL)
493493
await ERROR_LOGS.put(f"{self.path.name}: {e}")
494-
if self.proc.returncode is not None:
494+
if self.proc.returncode is None:
495495
await terminate_proc(self.proc)
496496
raise
497497
except asyncio.CancelledError:
498498
self.bar.set_job_status(Status.CANCELLED)
499+
if self.proc.returncode is None:
500+
await terminate_proc(self.proc)
499501
raise
500502

501503
self.bar.set_job_status(Status.DONE)
@@ -599,10 +601,12 @@ async def run(params):
599601
params["batch"],
600602
params["in_place"])))
601603

602-
asyncio.get_running_loop().add_signal_handler(
603-
signal.SIGINT,
604-
lambda: asyncio.ensure_future(sigint_handler(tasks))
605-
)
604+
loop = asyncio.get_running_loop()
605+
for sig in (signal.SIGINT, signal.SIGTERM):
606+
loop.add_signal_handler(
607+
sig,
608+
lambda: asyncio.ensure_future(sigint_handler(tasks))
609+
)
606610

607611
results = await asyncio.gather(*tasks, return_exceptions=True)
608612
completed = results.count(None)

qubespdfconverter/test_client.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/python3
2+
3+
import asyncio
4+
import signal
5+
import unittest
6+
from pathlib import Path
7+
from unittest import mock
8+
9+
from qubespdfconverter.client import Job, PageError, run
10+
11+
12+
class DummyProc:
13+
def __init__(self):
14+
self.returncode = None
15+
self.terminated = False
16+
self.stdin = mock.Mock()
17+
self.stdout = mock.Mock()
18+
19+
def terminate(self):
20+
self.terminated = True
21+
self.returncode = -signal.SIGTERM
22+
23+
async def wait(self):
24+
if self.returncode is None:
25+
self.returncode = 0
26+
27+
28+
class TC_ClientCancel(unittest.IsolatedAsyncioTestCase):
29+
async def test_000_cancel_terminates_qrexec_proc(self):
30+
job = Job(Path("/tmp/test.pdf"), 0)
31+
proc = DummyProc()
32+
33+
with mock.patch("asyncio.create_subprocess_exec", new=mock.AsyncMock(return_value=proc)):
34+
job._setup = mock.AsyncMock()
35+
job._start = mock.AsyncMock(side_effect=asyncio.CancelledError)
36+
37+
with self.assertRaises(asyncio.CancelledError):
38+
await job.run(Path("/tmp/archive"), depth=1, in_place=False)
39+
40+
self.assertTrue(proc.terminated)
41+
42+
async def test_001_failure_terminates_qrexec_proc(self):
43+
job = Job(Path("/tmp/test.pdf"), 0)
44+
proc = DummyProc()
45+
46+
with mock.patch("asyncio.create_subprocess_exec", new=mock.AsyncMock(return_value=proc)):
47+
job._setup = mock.AsyncMock(side_effect=PageError)
48+
49+
with self.assertRaises(PageError):
50+
await job.run(Path("/tmp/archive"), depth=1, in_place=False)
51+
52+
self.assertTrue(proc.terminated)
53+
54+
async def test_002_register_sigterm_handler(self):
55+
loop = asyncio.get_running_loop()
56+
add_handler_mock = mock.Mock()
57+
58+
with mock.patch.object(loop, "add_signal_handler", new=add_handler_mock):
59+
result = await run({
60+
"resolution": 300,
61+
"files": [],
62+
"archive": Path("/tmp/archive"),
63+
"batch": 1,
64+
"in_place": False,
65+
})
66+
67+
self.assertFalse(result)
68+
handled = {call.args[0] for call in add_handler_mock.mock_calls}
69+
self.assertEqual(handled, {signal.SIGINT, signal.SIGTERM})
70+
71+
72+
if __name__ == "__main__":
73+
unittest.main()

qvm-convert-pdf.gnome

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,21 @@ fi
2626

2727
export PROGRESS_FOR_GUI="yes"
2828

29-
/usr/bin/qvm-convert-pdf "$@" | zenity --progress --text="Converting PDF using Disposable VM..." --auto-close --auto-kill
29+
fifo="$(mktemp -u)"
30+
mkfifo "$fifo" || exit 1
31+
32+
/usr/bin/qvm-convert-pdf "$@" >"$fifo" &
33+
converter_pid="$!"
34+
35+
zenity --progress --text="Converting PDF using Disposable VM..." --auto-close <"$fifo"
36+
zenity_rc="$?"
37+
38+
rm -f "$fifo"
39+
40+
if [ "$zenity_rc" -ne 0 ]; then
41+
kill -TERM "$converter_pid" 2>/dev/null
42+
wait "$converter_pid" 2>/dev/null
43+
exit 1
44+
fi
45+
46+
wait "$converter_pid"

0 commit comments

Comments
 (0)