Fix 15-second close delay after dialog tests
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 <noreply@anthropic.com>
This commit is contained in:
@@ -228,7 +228,7 @@ class JsonRpcConnection:
|
|||||||
self.recv_thread.join()
|
self.recv_thread.join()
|
||||||
|
|
||||||
def is_alive(self):
|
def is_alive(self):
|
||||||
self.recv_thread.is_alive()
|
return self.recv_thread.is_alive()
|
||||||
|
|
||||||
def wait_ready(self, timeout=None):
|
def wait_ready(self, timeout=None):
|
||||||
return self._event_ready.wait(timeout)
|
return self._event_ready.wait(timeout)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from multiprocessing import Process, Pipe
|
import multiprocessing
|
||||||
|
|
||||||
from interpreter.test_items.test_item import TestItem
|
from interpreter.test_items.test_item import TestItem
|
||||||
|
|
||||||
|
_spawn_ctx = multiprocessing.get_context('spawn')
|
||||||
|
|
||||||
|
|
||||||
class TestItemDialogBase(TestItem):
|
class TestItemDialogBase(TestItem):
|
||||||
"""Base class for test items that launch a Qt dialog in a subprocess."""
|
"""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.
|
Returns the subprocess exit code.
|
||||||
"""
|
"""
|
||||||
p = Process(target=target, args=(args,))
|
p = _spawn_ctx.Process(target=target, args=(args,))
|
||||||
p.start()
|
p.start()
|
||||||
while p.is_alive() and not self._is_stopped:
|
while p.is_alive() and not self._is_stopped:
|
||||||
p.join(timeout=0.5)
|
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.
|
Returns the received value, or None if stopped or if the subprocess crashed.
|
||||||
"""
|
"""
|
||||||
parent_conn, child_conn = Pipe()
|
parent_conn, child_conn = _spawn_ctx.Pipe()
|
||||||
p = Process(target=target, args=(args, child_conn))
|
p = _spawn_ctx.Process(target=target, args=(args, child_conn))
|
||||||
p.start()
|
p.start()
|
||||||
|
child_conn.close()
|
||||||
result = None
|
result = None
|
||||||
while p.is_alive() and not self._is_stopped:
|
while p.is_alive() and not self._is_stopped:
|
||||||
if parent_conn.poll(0.5):
|
if parent_conn.poll(0.5):
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ class TestFileManager:
|
|||||||
):
|
):
|
||||||
w.test_service.stop()
|
w.test_service.stop()
|
||||||
w.test_service.close()
|
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
|
del w.test_proc
|
||||||
w.test_proc = None
|
w.test_proc = None
|
||||||
del w.test_service
|
del w.test_service
|
||||||
|
|||||||
Reference in New Issue
Block a user