From 2cc42e90652fc3d85360dc723d5d95d7204df037 Mon Sep 17 00:00:00 2001 From: francois Date: Mon, 20 Apr 2026 21:14:33 +0200 Subject: [PATCH] Fix 15-second close delay after dialog tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dialog subprocesses were forked from TestProcess, inheriting its multiprocessing Queue objects and their process-shared POSIX semaphores (_wlock). If a fork happened while the feeder thread held _wlock, the child exited without releasing it, permanently blocking the feeder thread on the next wacquire() and stalling Python's atexit _finalize_join — causing test_proc.join() (no timeout) to hang the app for ~15 seconds. Fix: use multiprocessing.get_context('spawn') for dialog subprocesses so they start with a clean interpreter and inherit no semaphores or Queue state. Also add a terminate/kill fallback timeout to test_proc.join() as a safety net, and fix the missing return in JsonRpcConnection.is_alive(). Co-Authored-By: Claude Sonnet 4.6 --- src/lib/jrpc.py | 2 +- .../interpreter/test_items/test_item_dialog_base.py | 11 +++++++---- src/testium/main_win/test_file_manager.py | 8 +++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib/jrpc.py b/src/lib/jrpc.py index 0b0cc9c..0820184 100644 --- a/src/lib/jrpc.py +++ b/src/lib/jrpc.py @@ -228,7 +228,7 @@ class JsonRpcConnection: self.recv_thread.join() def is_alive(self): - self.recv_thread.is_alive() + return self.recv_thread.is_alive() def wait_ready(self, timeout=None): return self._event_ready.wait(timeout) diff --git a/src/testium/interpreter/test_items/test_item_dialog_base.py b/src/testium/interpreter/test_items/test_item_dialog_base.py index ce11cf2..f6056b4 100644 --- a/src/testium/interpreter/test_items/test_item_dialog_base.py +++ b/src/testium/interpreter/test_items/test_item_dialog_base.py @@ -1,7 +1,9 @@ -from multiprocessing import Process, Pipe +import multiprocessing from interpreter.test_items.test_item import TestItem +_spawn_ctx = multiprocessing.get_context('spawn') + class TestItemDialogBase(TestItem): """Base class for test items that launch a Qt dialog in a subprocess.""" @@ -19,7 +21,7 @@ class TestItemDialogBase(TestItem): Returns the subprocess exit code. """ - p = Process(target=target, args=(args,)) + p = _spawn_ctx.Process(target=target, args=(args,)) p.start() while p.is_alive() and not self._is_stopped: p.join(timeout=0.5) @@ -31,9 +33,10 @@ class TestItemDialogBase(TestItem): Returns the received value, or None if stopped or if the subprocess crashed. """ - parent_conn, child_conn = Pipe() - p = Process(target=target, args=(args, child_conn)) + parent_conn, child_conn = _spawn_ctx.Pipe() + p = _spawn_ctx.Process(target=target, args=(args, child_conn)) p.start() + child_conn.close() result = None while p.is_alive() and not self._is_stopped: if parent_conn.poll(0.5): diff --git a/src/testium/main_win/test_file_manager.py b/src/testium/main_win/test_file_manager.py index 95158a2..fa35b14 100644 --- a/src/testium/main_win/test_file_manager.py +++ b/src/testium/main_win/test_file_manager.py @@ -30,7 +30,13 @@ class TestFileManager: ): w.test_service.stop() w.test_service.close() - w.test_proc.join() + w.test_proc.join(timeout=5) + if w.test_proc.is_alive(): + w.test_proc.terminate() + w.test_proc.join(timeout=2) + if w.test_proc.is_alive(): + w.test_proc.kill() + w.test_proc.join() del w.test_proc w.test_proc = None del w.test_service