From bd02e4b4c7516d2df90cb0aa3a6eba0f6f264b11 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sat, 10 Oct 2026 20:34:30 +0800 Subject: [PATCH] Clarify asynchronous cleanup ownership Signed-off-by: Loren Eteval --- Furious/Application/AGENTS.md | 7 +++++-- Furious/Controllers/AGENTS.md | 4 ++++ Furious/Core/AGENTS.md | 6 ++++++ Furious/Frozenlib/AGENTS.md | 6 ++++++ Furious/Interface/AGENTS.md | 4 ++++ Furious/Service/AGENTS.md | 20 ++++++++++++++++---- tests/README.md | 8 +++++++- 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Furious/Application/AGENTS.md b/Furious/Application/AGENTS.md index db99f3e6..17f055c8 100644 --- a/Furious/Application/AGENTS.md +++ b/Furious/Application/AGENTS.md @@ -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 diff --git a/Furious/Controllers/AGENTS.md b/Furious/Controllers/AGENTS.md index 5004c7e9..55649928 100644 --- a/Furious/Controllers/AGENTS.md +++ b/Furious/Controllers/AGENTS.md @@ -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. diff --git a/Furious/Core/AGENTS.md b/Furious/Core/AGENTS.md index 526f7386..8c20f178 100644 --- a/Furious/Core/AGENTS.md +++ b/Furious/Core/AGENTS.md @@ -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. diff --git a/Furious/Frozenlib/AGENTS.md b/Furious/Frozenlib/AGENTS.md index 3541114f..09a333f6 100644 --- a/Furious/Frozenlib/AGENTS.md +++ b/Furious/Frozenlib/AGENTS.md @@ -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 diff --git a/Furious/Interface/AGENTS.md b/Furious/Interface/AGENTS.md index c56a15de..4103767d 100644 --- a/Furious/Interface/AGENTS.md +++ b/Furious/Interface/AGENTS.md @@ -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 diff --git a/Furious/Service/AGENTS.md b/Furious/Service/AGENTS.md index e6d96cd9..bed5e7f1 100644 --- a/Furious/Service/AGENTS.md +++ b/Furious/Service/AGENTS.md @@ -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 diff --git a/tests/README.md b/tests/README.md index 43de7cb7..0aae8097 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 |