mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-13 09:48:10 +03:00
Keep disconnect and quit responsive
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+235
-1
@@ -66,13 +66,15 @@ from Furious.Service.LogManager import LogManager, TUN_LOG_CATEGORY
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QLabel, QStyle, QStyleOptionFrame, QWidget
|
||||
from shiboken6 import isValid
|
||||
from shiboken6 import isValid, delete as deleteQObject
|
||||
|
||||
from tests.support import application, isolatedSettings, processQtEvents, waitFor
|
||||
from tests.test_connection_startup_async import _Configuration, _Registry, _Runtime
|
||||
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from types import SimpleNamespace
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
@@ -1336,6 +1338,238 @@ class SingTUNChildTest(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
application()
|
||||
|
||||
@contextmanager
|
||||
def _delayedLinuxRecovery(self, *, denied=False):
|
||||
routing = importlib.import_module('Furious.Frozenlib.SingTUNRouting')
|
||||
controllers = importlib.import_module(
|
||||
'Furious.Controllers.ConnectionController'
|
||||
)
|
||||
plan = SingTUNHostPlan({}, platform='Linux')
|
||||
tun = plan.configuration['tun_options']
|
||||
tun['Name'] = 'utun101'
|
||||
plan._linux = helper = SingTUNLinuxRouting(tun, 123456, 2000, threading.Event())
|
||||
helper.index = 77
|
||||
helper.dnsTouched = plan._activated = True
|
||||
entered, authorized = threading.Event(), threading.Event()
|
||||
present, refused, processes = [True], [denied], []
|
||||
|
||||
def command(arguments):
|
||||
if not present[0]:
|
||||
return '[]'
|
||||
if arguments == ['ip', '-N', '-j', 'link', 'show']:
|
||||
return '[{"ifname":"utun101","ifindex":77}]'
|
||||
if arguments == ['ip', '-4', '-N', '-j', 'rule', 'show']:
|
||||
return '[{"priority":2001,"src":"all","table":123456,"suppress_prefixlen":0}]'
|
||||
return '[]'
|
||||
|
||||
def launch(arguments, **kwargs):
|
||||
self.assertEqual(arguments[:2], ['pkexec', 'bash'])
|
||||
process = mock.Mock()
|
||||
process.poll.return_value = None
|
||||
|
||||
def wait(timeout=None):
|
||||
self.assertIsNone(timeout)
|
||||
entered.set()
|
||||
if not authorized.wait(10):
|
||||
raise RuntimeError('fixture authorization was not released')
|
||||
code = 126 if refused[0] else 0
|
||||
process.poll.return_value = code
|
||||
if not code:
|
||||
present[0] = False
|
||||
return code
|
||||
|
||||
process.wait.side_effect = wait
|
||||
processes.append(process)
|
||||
return process
|
||||
|
||||
with isolatedSettings(), mock.patch.object(
|
||||
routing, '_command', side_effect=command
|
||||
), mock.patch.object(
|
||||
routing.os, 'geteuid', return_value=1000, create=True
|
||||
), mock.patch.object(
|
||||
routing.subprocess, 'Popen', side_effect=launch
|
||||
), mock.patch.object(
|
||||
controllers.SystemProxy, 'off'
|
||||
), mock.patch.object(
|
||||
controllers.Mixins.ConnectionAware, 'callDisconnectedCallback'
|
||||
), mock.patch(
|
||||
'sys.excepthook'
|
||||
) as qtErrors:
|
||||
manager = ConnectionManager()
|
||||
runtime = SingTUN({}, hostPlan=plan)
|
||||
router = RuntimeEventRouter()
|
||||
router.attach(runtime, manager)
|
||||
lease = RuntimeLease(runtime, router, applicationTUN=True)
|
||||
manager._leases.append(lease)
|
||||
controller = ConnectionController(
|
||||
coreManager=manager, updatesManager=mock.Mock()
|
||||
)
|
||||
controller._state = ConnectionState.Connected
|
||||
try:
|
||||
yield SimpleNamespace(
|
||||
controller=controller,
|
||||
manager=manager,
|
||||
runtime=runtime,
|
||||
lease=lease,
|
||||
plan=plan,
|
||||
helper=helper,
|
||||
entered=entered,
|
||||
authorized=authorized,
|
||||
processes=processes,
|
||||
refused=refused,
|
||||
present=present,
|
||||
)
|
||||
finally:
|
||||
refused[0] = False
|
||||
authorized.set()
|
||||
operation = manager.stopAsync()
|
||||
operation.start()
|
||||
self.assertTrue(waitFor(lambda: operation._terminal, timeout=10))
|
||||
manager.cleanup()
|
||||
controller.deleteLater()
|
||||
processQtEvents()
|
||||
qtErrors.assert_not_called()
|
||||
|
||||
def testDisconnectAuthorizationLeavesQtResponsiveAndKeepsItsLease(self):
|
||||
with self._delayedLinuxRecovery() as fixture:
|
||||
controller, runtime = fixture.controller, fixture.runtime
|
||||
states, ticks = [], []
|
||||
controller.stateChanged.connect(states.append)
|
||||
self.assertTrue(controller.startDisconnection())
|
||||
self.assertTrue(fixture.entered.wait(2))
|
||||
QtCore.QTimer.singleShot(0, lambda: ticks.append(True))
|
||||
self.assertTrue(waitFor(lambda: bool(ticks)))
|
||||
self.assertEqual(controller.state, ConnectionState.Disconnecting)
|
||||
self.assertTrue(runtime.cleanupPending)
|
||||
self.assertFalse(runtime._control.closed)
|
||||
self.assertEqual(fixture.manager._pendingReleases, [fixture.lease])
|
||||
self.assertEqual(len(fixture.processes), 1)
|
||||
with self.assertRaisesRegex(RuntimeError, 'cleanup is incomplete'):
|
||||
fixture.manager.startAsync(None, '')
|
||||
|
||||
fixture.authorized.set()
|
||||
self.assertTrue(
|
||||
waitFor(lambda: controller.state is ConnectionState.Disconnected)
|
||||
)
|
||||
self.assertEqual(
|
||||
states, [ConnectionState.Disconnecting, ConnectionState.Disconnected]
|
||||
)
|
||||
self.assertFalse(runtime.cleanupPending)
|
||||
self.assertTrue(runtime._control.closed)
|
||||
self.assertIsNone(fixture.plan._worker)
|
||||
self.assertIsNone(fixture.helper._process)
|
||||
self.assertEqual(fixture.manager._pendingReleases, [])
|
||||
|
||||
def testCancelledRecoveryAuthorizationIsAFailureWithoutAnAutomaticSecondPrompt(
|
||||
self,
|
||||
):
|
||||
with self._delayedLinuxRecovery(denied=True) as fixture:
|
||||
fixture.controller.startDisconnection()
|
||||
self.assertTrue(fixture.entered.wait(2))
|
||||
fixture.authorized.set()
|
||||
self.assertTrue(
|
||||
waitFor(
|
||||
lambda: fixture.controller.state is ConnectionState.Disconnected
|
||||
)
|
||||
)
|
||||
self.assertTrue(fixture.controller._runtimeCleanupFailed)
|
||||
self.assertEqual(len(fixture.processes), 1)
|
||||
self.assertEqual(fixture.manager._pendingReleases, [fixture.lease])
|
||||
self.assertFalse(fixture.runtime._control.closed)
|
||||
|
||||
def testReconnectWaitsForHostReleaseAndIsRequestedOnce(self):
|
||||
with self._delayedLinuxRecovery() as fixture, mock.patch.object(
|
||||
fixture.controller, 'startConnection', return_value=True
|
||||
) as reconnect:
|
||||
fixture.controller.startReconnection()
|
||||
self.assertTrue(fixture.entered.wait(2))
|
||||
processQtEvents()
|
||||
reconnect.assert_not_called()
|
||||
fixture.authorized.set()
|
||||
self.assertTrue(waitFor(lambda: reconnect.call_count == 1))
|
||||
self.assertEqual(fixture.manager._pendingReleases, [])
|
||||
|
||||
def testDestroyingTheCleanupObserverRetainsItsWorkerForRetry(self):
|
||||
with self._delayedLinuxRecovery() as fixture:
|
||||
fixture.controller.startDisconnection()
|
||||
self.assertTrue(fixture.entered.wait(2))
|
||||
operation = fixture.controller._stopOperation
|
||||
deleteQObject(operation)
|
||||
self.assertFalse(isValid(operation._timer))
|
||||
self.assertIsNone(fixture.manager._activeStopOperation)
|
||||
self.assertEqual(fixture.manager._pendingReleases, [fixture.lease])
|
||||
self.assertEqual(fixture.controller.state, ConnectionState.Disconnected)
|
||||
self.assertIsNotNone(fixture.helper._process)
|
||||
|
||||
def testShutdownWaitsAsynchronouslyAndPreservesTheStartupPreference(self):
|
||||
with self._delayedLinuxRecovery() as fixture:
|
||||
AppSettings.turnON_('Connect')
|
||||
results, ticks = [], []
|
||||
fixture.controller.shutdownFinished.connect(results.append)
|
||||
self.assertIs(fixture.controller.shutdown(), False)
|
||||
self.assertTrue(fixture.entered.wait(2))
|
||||
QtCore.QTimer.singleShot(0, lambda: ticks.append(True))
|
||||
self.assertTrue(waitFor(lambda: bool(ticks)))
|
||||
self.assertEqual(results, [])
|
||||
self.assertTrue(AppSettings.isStateON_('Connect'))
|
||||
fixture.authorized.set()
|
||||
self.assertTrue(waitFor(lambda: results == [True]))
|
||||
self.assertTrue(AppSettings.isStateON_('Connect'))
|
||||
self.assertIs(fixture.controller.shutdown(), True)
|
||||
self.assertEqual(len(fixture.processes), 1)
|
||||
|
||||
def testCancellingPreparationWaitsForItsWorkerWithoutBlockingQt(self):
|
||||
controllers = importlib.import_module(
|
||||
'Furious.Controllers.ConnectionController'
|
||||
)
|
||||
entered, released = threading.Event(), threading.Event()
|
||||
plan = SingTUNHostPlan({}, platform='Linux')
|
||||
|
||||
def prepare(_addresses):
|
||||
entered.set()
|
||||
if not released.wait(5):
|
||||
raise RuntimeError('fixture preparation was not released')
|
||||
|
||||
with isolatedSettings(), mock.patch.object(
|
||||
plan, 'prepare', side_effect=prepare
|
||||
), mock.patch.object(controllers.SystemProxy, 'off'), mock.patch.object(
|
||||
controllers.Mixins.ConnectionAware, 'callDisconnectedCallback'
|
||||
):
|
||||
manager = ConnectionManager()
|
||||
runtime = SingTUN({}, hostPlan=plan)
|
||||
router = RuntimeEventRouter()
|
||||
router.attach(runtime, manager)
|
||||
lease = RuntimeLease(runtime, router, applicationTUN=True)
|
||||
manager._leases.append(lease)
|
||||
controller = ConnectionController(
|
||||
coreManager=manager, updatesManager=mock.Mock()
|
||||
)
|
||||
controller._state = ConnectionState.Connecting
|
||||
plan.begin('prepare', ['192.0.2.1'])
|
||||
|
||||
try:
|
||||
self.assertTrue(entered.wait(2))
|
||||
controller.startDisconnection()
|
||||
ticks = []
|
||||
QtCore.QTimer.singleShot(0, lambda: ticks.append(True))
|
||||
self.assertTrue(waitFor(lambda: bool(ticks)))
|
||||
self.assertEqual(controller.state, ConnectionState.Disconnecting)
|
||||
self.assertEqual(manager._pendingReleases, [lease])
|
||||
released.set()
|
||||
self.assertTrue(
|
||||
waitFor(lambda: controller.state is ConnectionState.Disconnected)
|
||||
)
|
||||
self.assertIsNone(plan._worker)
|
||||
self.assertTrue(runtime._control.closed)
|
||||
finally:
|
||||
released.set()
|
||||
cleanup = manager.stopAsync()
|
||||
cleanup.start()
|
||||
self.assertTrue(waitFor(lambda: cleanup._terminal))
|
||||
manager.cleanup()
|
||||
controller.deleteLater()
|
||||
processQtEvents()
|
||||
|
||||
def testProductionSpawnImportsBindingAndStopsCooperatively(self):
|
||||
"""Exercise the production importer without opening a native TUN."""
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
Reference in New Issue
Block a user