mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-03 20:38:10 +03:00
Dispose output resources when runtime preparation fails
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user