Fix native runtime lifecycle ordering

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-22 21:28:19 +08:00
parent 3c0b3def25
commit b89f900758
3 changed files with 150 additions and 15 deletions
+8 -9
View File
@@ -484,17 +484,16 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
if AppSettings.get('SystemProxyMode') == AppBuiltinProxyMode.Auto.value:
SystemProxy.off()
SystemProxy.daemonOn_()
self._cleanupStack.register(
'automatic system proxy', self._cleanupSystemProxy
)
# sysproxy's change-notification daemon is a Windows-only resource.
# ConnectionController owns the configured OS proxy and turns it
# off during disconnection/shutdown on every platform.
if PLATFORM == 'Windows':
SystemProxy.daemonOn_()
@staticmethod
def _cleanupSystemProxy():
"""Release the exact automatic system-proxy integration."""
SystemProxy.off()
SystemProxy.daemonOff()
self._cleanupStack.register(
'Windows system proxy daemon', SystemProxy.daemonOff
)
def _initializeUI(self):
"""Create and bootstrap the application-owned main window and tray."""
+4 -6
View File
@@ -511,14 +511,12 @@ class ProcessOutputRedirector:
time.sleep(MsgQueue.MSG_PRODUCE_THRESHOLD / 1000)
# TODO: try to resolve messages not shown in edge case
time.sleep(2)
msgThread = threading.Thread(target=produceMsg, daemon=True)
msgThread.start()
# TODO: try to resolve messages not shown in edge case
time.sleep(2)
# The reader starts at the beginning of the temporary file, so output
# written before its first scheduling slice is still consumed. Do not
# delay the actual core entry point: the parent startup contract already
# provides its bounded readiness interval.
with tmpFileStream:
entrypoint()
+138
View File
@@ -30,11 +30,15 @@ from PySide6 import QtCore
from types import SimpleNamespace
from unittest import TestCase, mock
import importlib
import os
import sys
import tempfile
import textwrap
import subprocess
CoreProcessWorkerModule = importlib.import_module('Furious.Core.CoreProcessWorker')
class _Runtime(CoreRuntime):
"""Record exact runtime ownership without starting a host process."""
@@ -223,6 +227,140 @@ class ApplicationLifecycleTransactionTest(TestCase):
runListener.assert_called_once_with()
stopListener.assert_called_once_with()
def testSystemIntegrationSkipsWindowsProxyDaemonOnMacOS(self):
"""Do not register a nonexistent native proxy-daemon resource on macOS."""
cleanupStack = _ApplicationCleanupStack()
application = SimpleNamespace(
_cleanupStack=cleanupStack,
_sessionShutdownRequested=SimpleNamespace(emit=mock.Mock()),
setQuitOnLastWindowClosed=mock.Mock(),
)
with (
mock.patch(
'Furious.Application.DesktopApplication.Win32Session.set',
return_value=False,
),
mock.patch(
'Furious.Application.DesktopApplication.AppSettings.get',
return_value='Auto',
),
mock.patch(
'Furious.Application.DesktopApplication.PLATFORM',
'Darwin',
),
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.off'
) as proxyOff,
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.daemonOn_'
) as daemonOn,
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.daemonOff'
) as daemonOff,
):
DesktopApplication._initializeSystemIntegration(application)
cleanupStack.close()
proxyOff.assert_called_once_with()
daemonOn.assert_not_called()
daemonOff.assert_not_called()
def testSystemIntegrationOwnsOnlyWindowsProxyDaemonCleanup(self):
"""Keep OS proxy state under the connection controller's ownership."""
cleanupStack = _ApplicationCleanupStack()
application = SimpleNamespace(
_cleanupStack=cleanupStack,
_sessionShutdownRequested=SimpleNamespace(emit=mock.Mock()),
setQuitOnLastWindowClosed=mock.Mock(),
)
with (
mock.patch(
'Furious.Application.DesktopApplication.Win32Session.set',
return_value=False,
),
mock.patch(
'Furious.Application.DesktopApplication.AppSettings.get',
return_value='Auto',
),
mock.patch(
'Furious.Application.DesktopApplication.PLATFORM',
'Windows',
),
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.off'
) as proxyOff,
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.daemonOn_'
) as daemonOn,
mock.patch(
'Furious.Application.DesktopApplication.SystemProxy.daemonOff'
) as daemonOff,
):
DesktopApplication._initializeSystemIntegration(application)
cleanupStack.close()
proxyOff.assert_called_once_with()
daemonOn.assert_called_once_with()
daemonOff.assert_called_once_with()
def testProcessOutputRedirectorDoesNotDelayCoreEntrypoint(self):
"""Begin the core immediately while its file reader drains early output."""
class _Stream:
def __init__(self, descriptor):
self.descriptor = descriptor
def fileno(self):
return self.descriptor
def close(self):
pass
class _TemporaryDir:
def __init__(self, directory):
self.directory = directory
def isValid(self):
return True
def filePath(self, name):
return os.path.join(self.directory, name)
with tempfile.TemporaryDirectory() as directory:
thread = SimpleNamespace(start=mock.Mock())
entrypoint = mock.Mock()
fakeSys = SimpleNamespace(stdout=_Stream(1), stderr=_Stream(2))
with (
mock.patch.object(
CoreProcessWorkerModule.SystemRuntime,
'isPythonw',
return_value=False,
),
mock.patch.object(
CoreProcessWorkerModule.ProcessOutputRedirector,
'TemporaryDir',
_TemporaryDir(directory),
),
mock.patch.object(CoreProcessWorkerModule, 'sys', fakeSys),
mock.patch.object(CoreProcessWorkerModule.os, 'dup2'),
mock.patch.object(
CoreProcessWorkerModule.threading,
'Thread',
return_value=thread,
),
mock.patch.object(CoreProcessWorkerModule.time, 'sleep') as sleep,
):
CoreProcessWorkerModule.ProcessOutputRedirector.launch(
mock.Mock(), entrypoint, True
)
sleep.assert_not_called()
thread.start.assert_called_once_with()
entrypoint.assert_called_once_with()
def testFailuresAtMeaningfulStagesRollBackOnlyEarlierStages(self):
expected = {
'storage': ['cleanup storage', 'cleanup plugins'],