Clarify asynchronous cleanup ownership

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-10-10 20:34:30 +08:00
parent dfcdfd254f
commit bd02e4b4c7
7 changed files with 48 additions and 7 deletions
+5 -2
View File
@@ -17,8 +17,11 @@ relative to this source tree's root.
`aboutToQuit` and the event-loop `finally` may both reach it; repeated entry must not repeat registered stages.
One callback failure does not skip later stages, but the stack consumes that callback and does not retry it.
Its successful `close()` return means this invocation ran the stack, not that every resource was released.
Service-level retry/retention obligations must be satisfied before the owner disappears. `exit()` requests Qt
termination; action/window/session handlers do not run cleanup directly.
Service-level retry/retention obligations must be satisfied before the owner disappears. `exit()` first requests
controller shutdown while Qt delivery is still active, then requests Qt termination after successful connection
cleanup. Action/window/session handlers do not run the application cleanup stack directly. Pending or refused
quit must keep the main window usable; the application retains controllers whose shutdown has not completed.
`tests/test_architecture_refactors.py` exercises the real quit/event-loop boundary alongside cleanup ordering.
- A stage that fails before its cleanup callback is registered must release its own partial acquisitions. The outer
cleanup stack releases completed stages; it cannot discover half-built controllers, UI, logging handlers, or
native listeners. Keep each successful sub-acquisition reachable by the stage's failure cleanup before beginning
+4
View File
@@ -61,6 +61,10 @@ source tree's root.
new acquisition while they remain. Final controller shutdown surfaces unresolved cleanup and preserves the
next-start preference. Check these outcomes independently in `tests/test_controllers.py` and
`tests/test_runtime_lifecycle.py`.
Pending asynchronous cleanup stays `Disconnecting`, with runtime ownership in the service. Reconnect waits for
successful release; a genuine cleanup failure returns to usable UI without admitting replacement resources.
Shutdown publishes its result only after this stop operation finishes and preserves the startup preference.
`tests/test_sing_tun.py` covers delayed authorization, cancellation, reconnect and shutdown with real Qt delivery.
- System Proxy configuration is best effort. Its helper logs host failures; an explicit False return does not roll
back an otherwise usable committed runtime or prevent Connected. Preserve exception-path recovery separately.
Exercise actual helper results with mocked OS boundaries; Connected does not guarantee the OS proxy was applied.
+6
View File
@@ -57,6 +57,12 @@ relative to this source tree's root.
in `tests/test_sing_tun.py` cover startup cancellation, validated status with binding-provided failure reasons,
and cleanup refusal/retry. Privileged platform smoke tests remain separate from harmless source/compiled
lifetime checks.
On Linux the connection also owns an authorized host helper. Monitor its unexpected exit alongside the native
child and publish once through the same runtime route; publication does not establish that the child was reaped.
Pending Linux preparation/restoration sets `cleanupPending`; preserve IPC, timers and the host owner until that
work completes. Poll host workers without blocking Qt on authorization. Native-child stop remains bounded and
in its constructing Qt thread; do not move timer disposal into the host worker. Worker completion alone does not
establish that its thread and helper were reaped; finish their release before clearing pending status.
Binding-provided failure text is diagnostic input, not a stable enumerated protocol: preserve useful reasons rather
than accepting only exact known strings. Changing binding versions requires coordinated model validation,
distribution metadata/native-library inclusion, and workflow API checks; a successful import does not exercise TUN.
+6
View File
@@ -53,6 +53,12 @@ Read `Furious/Frozenlib/AppSettings.py` with `tests/test_frozenlib.py`; paths ar
managed routing state. The service holds one helper per attempt and supplies its cancellation event; this module
neither imports Service nor starts work during import. `tests/test_sing_tun_linux_host.py` covers the helper
boundary; `tests/test_sing_tun.py` covers its use by the service, with privileged Linux smoke testing separate.
Batch Linux setup and DNS into one fixed script. Keep that authorized helper until forwarding stops; closing its
stdin runs its exact cleanup, including on parent exit. It accepts no executable commands from the application.
Record successful rule additions in the helper for partial rollback. Kernel JSON read-back is a verification and
recovery boundary, not the source of normal teardown commands; its field spellings differ from CLI selectors.
PolicyKit authorization deliberately has no deadline, matching tun2socks. Keep it distinct from bounded host
reads and explicit cancellation/reaping waits; elapsed time alone must not expire a pending password request.
- Windows proxy calls, Linux desktop settings/host bridging, and macOS network-service operations are distinct
paths. Application tun2socks host routing differs from backend-native TUN; preserve privilege, DNS restoration,
and managed route cleanup for the selected path. Some helpers block synchronously and need caller-level
+4
View File
@@ -46,6 +46,10 @@ Read `Furious/Interface/Runtime.py` with `tests/test_interface.py`; paths are re
cleanup remains observable to its owner and report violations separately from the required contract. Do not
impose a Boolean release convention on `CoreRuntime.stop()` or `dispose()` merely because the service lease
returns a Boolean: the lease combines exceptions and passive liveness into its own release outcome.
`cleanupPending` separately reports asynchronous resource release. While true, callers retain the runtime and
poll stop/dispose from their owning thread; pending work is not a failed stop or a disposed runtime. False alone
is not release proof: exceptions and actual resource/liveness checks still apply. The default is false for
synchronous implementations; sing-tun's Linux restoration and the lease tests anchor the deferred case.
- Verify cheap/import-independent contracts plus representative runtime, storage, editor, application-exit,
encoding, and configuration implementations. Update this guide when a contract intentionally changes, together
with all implementers and compatibility tests. Start with `tests/test_interface.py` and
+16 -4
View File
@@ -58,11 +58,19 @@ source tree's root.
- For sing-tun, resolve automatic and manual remote exclusions before applying host routing, then require
native readiness and checked DNS application before commit. `SingTUNHostPlan` owns only its assigned identifiers
and DNS snapshots, independently of legacy `SystemRoutingTable.managedRoutes`; refuse ambiguous recovery and
retain ownership for retry. Host preparation/DNS commands run in its owned worker, while synchronous compatibility
startup and bounded cleanup joins remain explicit responsiveness limits. Delegate Linux host operations to
retain ownership for retry. Host preparation/DNS commands run in its owned worker. Linux authorization and its
startup observer have no deadline; other readiness checks keep their own deadlines. Linux restoration is polled
through the manager's stop operation rather than joined on Qt's thread. Synchronous startup remains a compatibility
path; synchronous stop callers report incomplete release while asynchronous cleanup is pending.
Delegate Linux host operations to
`Frozenlib/SingTUNRouting.py`; the service retains attempt orchestration, its worker and the helper's lifetime.
Linux uses an attempt-owned PolicyKit
helper to create a persistent TUN owned by the calling user and configure routes/DNS; the unprivileged binding
Linux uses one attempt-owned PolicyKit
helper to create a persistent TUN owned by the calling user and configure routes/DNS in one authorization. Keep
the helper through forwarding and close its control pipe after the native child is reaped, so ordinary disconnect
does not request another password. Unexpected helper exit fails the connection through its runtime owner.
If the Linux forwarding child cannot be reaped, retain its helper and per-link DNS; report restoration as deferred
instead of claiming DNS was restored while the helper remains active.
The unprivileged binding
receives external-configuration options only in its runtime copy. Windows/macOS retain native auto-routing.
Retain unfinished helpers and their cancellation scripts until they are reaped; cancellation does not grant
permission to forget an elevated process. Delete only an interface whose captured index matches, and
@@ -78,6 +86,10 @@ source tree's root.
retry and refuses new startup while release remains incomplete. A failed attempt transfers unreleased leases
back to that durable owner; deleting the attempt must not abandon them. Independent DNS cleanup still runs.
Verify runtime liveness and actual handle/thread release separately from the lease's logical state.
Pending cleanup is observed through the runtime contract and is not logged as failure. Stop failures retain the
runtime without immediately retrying via dispose, which could open another authorization request. The stop
operation owns its Qt timer; destroyed observers leave their leases with the manager. Retire stop admission before
terminal signals, preserve other runtimes' independent cleanup, and reject new startup until release completes.
Native startup-operation destruction cancels child observers and rolls back an uncommitted attempt before child
deletion. Terminal observers may destroy the operation synchronously; retire manager ownership without a later
call into the dead QObject. Failed release still transfers to the manager, and a committed lease remains its
+7 -1
View File
@@ -110,7 +110,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
| [test_frozenlib.py](test_frozenlib.py) | Nested state guards, cleanup isolation, native peer destruction during connection/theme/translation/cleanup notifications, bounded caches/throttling, dual-stack probe selection, mocked proxy/DNS/routes/startup/session boundaries and failure handling. |
| [test_native_tun_semantics.py](test_native_tun_semantics.py) | Xray/Hysteria2 runtime-copy TUN preservation/replacement, managed-TUN failures, download-test stripping, prevention of a second tun2socks owner. |
| [test_sing_tun.py](test_sing_tun.py) | tun2socks engine default, saved engine choices and ownership-aware reconnect notices, sing-tun Go stack default, development-stack and congestion-control compatibility, independent backend settings and host defaults, initial and live dialog-button translation, selected-only snapshots, captured TUN log sources in both startup paths, staged settings and malformed storage, SOCKS authentication/UDP, harmless real spawn/status/stop, native readiness, cancellation, synchronous compatibility, mocked DNS/routing ownership including development IPv6 rule ordering and refused cleanup. |
| [test_sing_tun_linux_host.py](test_sing_tun_linux_host.py) | Frozenlib sing-tun routing helpers: mocked normal-user and root execution, Bash syntax and isolated script rollback through a fake ip command, user-owned persistent TUN preparation, authorization cancellation/timeouts, retained helper/script ownership, dual-stack route exclusions and policy ordering, iproute2 JSON flags/prefix lengths, interface replacement refusal and exact cleanup retry. No real PolicyKit authorization, TUN, routes or DNS are changed. |
| [test_sing_tun_linux_host.py](test_sing_tun_linux_host.py) | Frozenlib sing-tun routing helpers: one authorization for setup/DNS/disconnect, authorization without a deadline and cancellation, retained helper/script ownership, real Bash lifetime and partial rollback through fake ip/resolvectl commands, parent-pipe EOF restoration, user-owned persistent TUN preparation, dual-stack route exclusions and policy ordering, iproute2 JSON flags/prefix lengths/suppression fields, interface replacement refusal and exact cleanup retry. No real PolicyKit authorization, TUN, routes or DNS are changed. |
| [test_subscription_sync.py](test_subscription_sync.py) | Group-local preparation/commit, stable duplicate identity, atomic failure, preservation of newer local metadata, rejection of changed source state. |
| [test_subscription_manager.py](test_subscription_manager.py) | Stable request generations, provider metadata, batch/partial failures, timer policy, targeted updates, real Stop Updates input, worker responsiveness, cancellation and synchronous shutdown ownership. |
| [test_subscription_scalability.py](test_subscription_scalability.py) | Deterministic 1/3/8-group preparation and commit with 1,500 profiles per group and bounded workers; uses the offline benchmark helper. |
@@ -122,6 +122,12 @@ worker. Choose tests by the changed contract rather than by filename alone.
| [test_log_manager_generation.py](test_log_manager_generation.py) | Reference-model state transitions, concurrency, live/retired ownership, bounded FIFO cleanup, structural complexity, incremental/multiline LogPage rendering; also contains an opt-in generation campaign. |
| [test_log_manager_benchmark.py](test_log_manager_benchmark.py) | Data-only report validation, workload compatibility, timing ratios, and real standalone CLI JSON round trips; no performance gates or historical imports. |
Linux sing-tun asynchronous disconnect is covered in `test_sing_tun.py`: delayed authorization with continued Qt
delivery, retained leases and blocked replacement, cancellation without a second prompt, deferred reconnect,
observer destruction and shutdown preferences. `test_runtime_lifecycle.py` checks stop-operation timer destruction;
`test_architecture_refactors.py` checks that real application exit keeps the event loop and main window available
until connection cleanup finishes. These tests mock host changes and do not exercise a real PolicyKit agent.
### Qt behavior, layout, and lifetime
| Module | Coverage |