Dispose output resources when runtime preparation fails

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-18 22:03:07 +08:00
parent e4f7c9f7dd
commit 87c06d8d1f
4 changed files with 41 additions and 4 deletions
+3 -1
View File
@@ -7,7 +7,9 @@ This scope owns reusable embedded execution machinery and application tun2socks;
Core owns its separate direct `subprocess.Popen`; neither layer owns controller, repository, UI, or protocol policy.
- A launch spec describes prepared child construction, never semantic connection readiness. Serialization and launch
arguments are prepared before execution starts; constructors may create owned timers/queues that still need
disposal if execution never starts. The service observes endpoints/process survival and commits later.
disposal if execution never starts. Failed launch-factory construction must dispose resources already acquired
before propagating the failure, because no service has received ownership yet. The service observes
endpoints/process survival and commits later.
- `CoreRuntime` execution state, typed terminal exit, and readiness are separate contracts. A process becoming alive is
not proof that its proxy/TUN endpoint is ready, while a readiness timeout must not overwrite an already observed typed
exit.
+7 -3
View File
@@ -86,12 +86,16 @@ class MultiprocessingRuntime(CoreRuntime):
self._exitPublished = False
self._output = MsgQueue(msgCallback=msgCallback)
launch = launchFactory(self._output)
try:
launch = launchFactory(self._output)
if not isinstance(launch, ProcessLaunchSpec):
if not isinstance(launch, ProcessLaunchSpec):
raise TypeError('launch factory must return ProcessLaunchSpec')
except BaseException:
# Construction has not transferred this resource to a service owner.
self._output.dispose()
raise TypeError('launch factory must return ProcessLaunchSpec')
raise
self._launch = launch
self._monitor = QtCore.QTimer()
+2
View File
@@ -227,3 +227,5 @@ instead.
`test_repository_contracts.py` verifies all-or-nothing hydration and preservation of
original stored bytes after malformed records or plugin parsing failures.
`test_runtime_lifecycle.py` verifies real Qt timer destruction and queue disposal when
launch preparation fails before a service acquires the runtime.
+29
View File
@@ -254,6 +254,35 @@ class RuntimeLifecycleTest(TestCase):
runtime.dispose()
def testLaunchFactoryFailureDisposesAcquiredOutput(self):
"""Release the real queue, callback, and Qt timer before returning failure."""
outputs = []
destroyed = []
def fail(output):
outputs.append(output)
output.timer.destroyed.connect(lambda: destroyed.append(True))
raise ValueError('fixture preparation failure')
runtime = _ProcessRuntime.__new__(_ProcessRuntime)
try:
with self.assertRaisesRegex(ValueError, 'fixture preparation failure'):
MultiprocessingRuntime.__init__(
runtime, fail, msgCallback=lambda _: None
)
processQtEvents()
self.assertEqual(destroyed, [True])
self.assertTrue(outputs[0]._closed)
self.assertIsNone(outputs[0].callback)
self.assertIsNone(outputs[0]._timerConnection)
finally:
outputs[0].dispose()
processQtEvents()
def testMultiprocessingSpawnFailureRaisesStructuredError(self):
"""Represent expected acquisition failure without mutable side state."""
process = mock.Mock()