Preserve runtime ownership after start failure

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-27 16:40:04 +08:00
parent 74cbd6f4f9
commit 16637ec545
3 changed files with 47 additions and 1 deletions
+3
View File
@@ -19,6 +19,9 @@
- Registries strongly own process-lifetime plugin instances, capabilities/factories, descriptors, and immutable metadata.
They do not own factory-created widgets, active runtimes, replies, or controller state. Factories return a fresh object
per request; invalid QObject results are explicitly destroyed.
- Once a runtime factory returns a valid launch, startup transfers that exact runtime to the calling connection attempt
even when `start()` raises, so the attempt can stop and dispose partial resources. A controlled failure returns `None`
only when no valid runtime was acquired.
- External entry points and plugin-returned data are a boundary even when plugins are trusted for execution. Validate
types and required fields, isolate optional provider failure where the operation can continue, and keep the primary
failure observable when it cannot.
+6 -1
View File
@@ -1100,6 +1100,8 @@ class PluginRegistry:
def startCoreRuntime(self, config, routing, **kwargs):
"""Create and start the core runtime selected for *config*."""
launch = None
try:
launch = self.createCoreRuntime(config, routing, **kwargs)
@@ -1116,7 +1118,10 @@ class PluginRegistry:
logger.error(f'core runtime start failed for {factoryId!r}: {ex}')
return None, False
# Once construction succeeds, ownership must still cross to the
# connection transaction even if start() raises. Returning None
# here would abandon a partially acquired process/thread/runtime.
return (launch.runtime if launch is not None else None), False
def prepareDownloadTest(self, config, port: int):
"""Create a proxy-only test configuration through its runtime factory."""
+38
View File
@@ -539,6 +539,44 @@ class PluginFailureIsolationTest(unittest.TestCase):
finally:
registry.shutdown()
def testCoreRuntimeStartExceptionReturnsRuntimeForCallerRollback(self):
"""Transfer a partially started runtime to the connection transaction."""
class RaisingStartRuntime(FixtureCoreRuntime):
def start(self, configuration, *args, **kwargs):
raise RuntimeError('start fixture')
runtime = RaisingStartRuntime()
class RaisingStartFactory(CoreRuntimeFactory):
factoryId = 'raising-start-factory'
configurationTypes = (FixtureConfiguration,)
runtimeTypes = (RaisingStartRuntime,)
def create(self, request: CoreRuntimeRequest):
return CoreRuntimeLaunch(runtime, request.configuration)
class RaisingStartPlugin(FuriousPlugin):
metadata = PluginMetadata(
'tests.raising-start-factory',
'Raising start factory',
)
capabilities = (RaisingStartFactory(),)
registry = PluginRegistry()
registry.register(RaisingStartPlugin())
try:
returnedRuntime, success = registry.startCoreRuntime(
FixtureConfiguration({'type': 'fixture'}),
'direct',
)
self.assertIs(returnedRuntime, runtime)
self.assertFalse(success)
finally:
registry.shutdown()
def testShutdownExceptionDoesNotBlockOtherPlugins(self):
"""Run every shutdown hook once even when one hook raises."""
stopped = []