Skip to content

refactor(runtime): drive every start through one RuntimeCoordinator - #189

Merged
YumeYuka merged 2 commits into
fix-daemon-lifecycle-launch-reapfrom
refactor-simplify-runtime-start-stop
Sep 27, 2026
Merged

YumeYuka merged 2 commits into
fix-daemon-lifecycle-launch-reapfrom
refactor-simplify-runtime-start-stop

Conversation

@YumeLira

@YumeLira YumeLira commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Replace the launcher/status-store/broadcast protocol with a single
in-process coordinator that serializes start, stop, reload and verify
behind one mutex and publishes a StateFlow.

  • UI, tile, auto-restart, Wi-Fi automation and config reloads all call
    the coordinator; the MMKV phase slot, session token and lifecycle
    broadcasts are gone.
  • Readiness probes GET /group with 25/50/100ms backoff instead of full
    /proxies queries every 200ms; group names are parsed only on demand.
  • Drop dead work on the start path: config fingerprints, the unused
    post-start snapshot refresh, the service log stream, no-op observers
    and the transport fallback compile.
  • VPN stop waits for TunService.onDestroy and the core pid; connection
    tracking polls only while the app is in the foreground.

Summary by Sourcery

Route every local runtime start, stop, reload, and verification operation through one serialized coordinator with a single observable state source.

New Features:

  • Centralize local runtime lifecycle control for UI, tile, automatic restart, Wi-Fi automation, and configuration changes through a single coordinator exposing a StateFlow runtime state.

Bug Fixes:

  • Make runtime startup and shutdown accurately reflect VPN service, core-process, and root-daemon readiness and termination.
  • Preserve and surface recent runtime startup failures across process restarts.

Enhancements:

  • Replace persisted lifecycle phases, session tokens, launcher protocols, and lifecycle broadcasts with serialized in-process coordination.
  • Reduce startup probing and runtime polling overhead, including foreground-only connection tracking and on-demand proxy-group parsing.
  • Simplify core query APIs and remove obsolete runtime abstractions, observers, logging streams, fingerprints, and transport fallback paths.

Chores:

  • Consolidate VPN and root runtime session management around shared lifecycle and configuration handling.

@sourcery-ai

sourcery-ai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This refactor replaces the distributed launcher, persisted-status, session-token, and broadcast lifecycle protocol with a single mutex-serialized RuntimeCoordinator exposing RuntimeState via StateFlow, while simplifying startup probes and config compilation and tightening VPN/root shutdown and recovery behavior.

Sequence diagram for serialized runtime start and state publication

sequenceDiagram
    participant Caller
    participant C as RuntimeCoordinator
    participant M as mutex
    participant Host as TunService or RootRuntime
    participant Core
    participant State as StateFlow<RuntimeState>
    Caller->>C: start(mode, source)
    C->>M: runOp(startLocked)
    M-->>C: lock acquired
    C->>State: publish Starting
    C->>Host: start or launch
    Host->>Core: start process and load config
    Core-->>Host: controller ready
    Host-->>C: start complete
    C->>State: publish Running
    C-->>Caller: return
Loading

Sequence diagram for coordinated stop and VPN teardown

sequenceDiagram
    participant Caller
    participant C as RuntimeCoordinator
    participant M as mutex
    participant Session as VpnSession
    participant Service as TunService
    participant Core
    participant State as StateFlow<RuntimeState>
    Caller->>C: stop(reason)
    C->>M: runOp(stopLocked)
    M-->>C: lock acquired
    C->>State: publish Stopping
    C->>Session: stop()
    Session->>Core: stop and wait for process exit
    C->>Service: finish()
    Service-->>C: onDestroy
    C->>State: publish Idle
    C-->>Caller: return
Loading

State diagram for RuntimeCoordinator lifecycle

stateDiagram-v2
    [*] --> Idle
    Idle --> Starting: start(mode, source)
    Starting --> Running: controller ready
    Starting --> Failed: startup error
    Starting --> Idle: cancellation or cleanup
    Running --> Reloading: reload()
    Reloading --> Running: reload accepted
    Reloading --> Running: reload rejected and restored
    Reloading --> Failed: runtime unavailable
    Running --> Stopping: stop(reason)
    Running --> Idle: verify() detects vanished core
    Stopping --> Idle: teardown complete
    Failed --> Starting: start(mode, source)
    Failed --> Idle: stop(reason)
Loading

Flow diagram for runtime readiness probing

flowchart TD
    Start[Core launched] --> Probe[ControllerReadiness.await]
    Probe --> Alive{Core alive?}
    Alive -- No --> Fail[Fail startup]
    Alive -- Yes --> Group[GET /group]
    Group --> Ready{Groups ready or config has no groups?}
    Ready -- Yes --> Running[Publish Running]
    Ready -- No --> Backoff[Wait 25ms, 50ms, then 100ms]
    Backoff --> Probe
    Probe --> Timeout{2s budget exceeded?}
    Timeout -- Yes --> Fail
Loading

File-Level Changes

Change Details Files
Centralize local runtime lifecycle and state publication in a mutex-serialized coordinator.
  • Introduce RuntimeControl and RuntimeState StateFlow as the lifecycle API.
  • Move VPN, root-daemon, reload, verify, failure retention, and service recreation handling into RuntimeCoordinator.
  • Route UI/session, tile, auto-restart, Wi-Fi automation, and config changes through coordinator calls.
  • Remove launcher, ownership, persisted phase/session-token, lifecycle-broadcast, and legacy status-store protocols.
runtime/api/src/runtime/api/PlatformSeams.kt
runtime/api/src/runtime/api/Intents.kt
runtime/client/src/runtime/client/RuntimeRequests.kt
runtime/client/src/runtime/client/RuntimeStateMapper.kt
runtime/client/src/runtime/client/session/RuntimeSession.kt
runtime/client/src/runtime/client/session/RuntimeSessionDeps.kt
runtime/client/src/runtime/client/session/RuntimeEventBridge.kt
runtime/client/src/runtime/client/session/RuntimeRemoteSwitch.kt
runtime/client/src/runtime/client/session/RuntimeOwnership.kt
runtime/service/src/runtime/service/RuntimeCoordinator.kt
runtime/service/src/runtime/service/RuntimeFailureRecord.kt
runtime/service/src/runtime/service/StatusProvider.kt
runtime/service/src/runtime/service/TunService.kt
runtime/service/src/runtime/service/AndroidRuntimeStatusStore.kt
runtime/service/src/runtime/service/RuntimeForegroundController.kt
runtime/service/src/runtime/service/session/AndroidRuntimeLauncher.kt
runtime/service/src/runtime/service/session/RootSessionLauncher.kt
runtime/service/src/runtime/service/session/RuntimeHost.kt
runtime/service/src/runtime/service/session/RuntimeServiceLauncher.kt
runtime/service/src/runtime/service/core/AndroidProcessController.kt
Simplify startup and reload execution around one compiled runtime configuration with focused readiness probes.
  • Compile once per start or reload and retain the resulting YAML for transport launch.
  • Probe root readiness through the controller and VPN readiness through GET /group with 25/50/100ms backoff.
  • Parse expected group names only when the readiness probe reports zero groups.
  • Remove fingerprints, post-start snapshot refresh machinery, group preview compilation, transport fallback compilation, and unused runtime observers/log-stream plumbing.
runtime/service/src/runtime/service/session/VpnSession.kt
runtime/service/src/runtime/service/session/RootRuntime.kt
runtime/service/src/runtime/service/session/ControllerReadiness.kt
runtime/service/src/runtime/service/session/CompiledConfigPipeline.kt
runtime/service/src/runtime/service/session/RuntimeSpec.kt
runtime/service/src/runtime/service/core/CoreProcess.kt
runtime/service/src/runtime/service/controller/CoreController.kt
runtime/service/src/runtime/service/session/VpnTunTransport.kt
runtime/service/src/runtime/service/session/CompiledTunPackages.kt
runtime/service/src/runtime/service/session/RuntimeTransport.kt
runtime/service/src/runtime/service/session/SessionRuntime.kt
runtime/service/src/runtime/service/session/SessionRuntimeObservers.kt
runtime/service/src/runtime/service/session/SessionRuntimeTelemetry.kt
runtime/service/src/runtime/service/session/ServiceNetworkObserver.kt
Update all external runtime triggers and UI state consumers to use the coordinator state flow.
  • Make tile rendering collect RuntimeState directly and start or stop through RuntimeCoordinator.
  • Replace auto-restart and Wi-Fi automation launchers with coordinator start, stop, and verify operations.
  • Mirror coordinator state into the client RuntimeSnapshot while keeping payload and traffic readiness in the session layer.
  • Limit connection-history polling to periods when the app is foregrounded.
app/src/common/util/ProxyAutoStartHelper.kt
runtime/service/src/runtime/service/AutoRestartService.kt
runtime/service/src/runtime/service/ProxyTileService.kt
runtime/service/src/runtime/service/WifiAutomationService.kt
runtime/client/src/runtime/client/session/RuntimeGroupHub.kt
runtime/service/src/runtime/service/session/ConnectionTracker.kt
Strengthen VPN and root shutdown, process monitoring, and recovery behavior.
  • Wait for TunService destruction and the local core process during VPN teardown.
  • Watch VPN core liveness and transition unexpected exits to Failed.
  • Reattach surviving root daemons during coordinator bootstrap and retain launch timestamps.
  • Add bounded failure persistence for diagnostics while removing legacy runtime files and phase state.
runtime/service/src/runtime/service/RuntimeCoordinator.kt
runtime/service/src/runtime/service/TunService.kt
runtime/service/src/runtime/service/core/RootDaemonState.kt
runtime/service/src/runtime/service/RuntimeFailureRecord.kt
runtime/service/src/runtime/service/StatusProvider.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@YumeLira
YumeLira added this pull request to stack #190 September 27, 2026 10:44

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="runtime/service/src/runtime/service/RuntimeCoordinator.kt" line_range="169" />
<code_context>
+
+    internal fun onVpnServiceDestroyed(service: TunService) {
+        if (vpnService === service) vpnService = null
+        vpnDestroy?.complete(Unit)
+        if (activeVpn === service) {
+            scope.launch {
</code_context>
<issue_to_address>
**issue (bug_risk):** A late `onVpnServiceDestroyed` callback from an old `TunService` can complete the `vpnDestroy` deferred belonging to a newer service because the callback completes `vpnDestroy` before checking the service identity, and `stopVpnLocked` stores only an unassociated deferred. The coordinator then stops waiting early and can proceed while the current VPN service is still being destroyed.

**Triggers:** When a VPN service destruction exceeds the 3-second timeout and a replacement service is started before the old `onDestroy` callback arrives.

**Suggested fix:** Associate the destruction deferred with its expected service and complete it only when `service === expectedService`.
</issue_to_address>

### Comment 2
<location path="runtime/service/src/runtime/service/RuntimeCoordinator.kt" line_range="320-321" />
<code_context>
+    private suspend fun startVpnLocked(source: RuntimeStartSource) {
+        RuntimeLog.writer(context, RuntimeLog.Source.LocalTun)
+            .beginSession(RuntimeLog.Type.Launcher, "request start source=$source mode=VpnService")
+        val service = vpnService ?: awaitVpnService()
+        activeVpn = service
+        val spec = withContext(Dispatchers.IO) { SessionRuntimeSpecFactory(context).createVpnSpec() }
+        service.session.start(spec)
</code_context>
<issue_to_address>
**issue (bug_risk):** `startVpnLocked` reuses any non-null `vpnService` without verifying that the instance is still alive. After `stopVpnLocked` times out waiting for destruction, `vpnService` remains set until the delayed callback, so a new start can call `session.start` on the old service instance while its teardown is still pending.

**Triggers:** When `TunService.onDestroy` is delayed beyond `VPN_DESTROY_TIMEOUT_MS` and another start follows immediately.

**Suggested fix:** Clear or invalidate `vpnService` before allowing a replacement start, and await or explicitly reject starts while the previous service is still being destroyed.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and a lifecycle error in the new coordinator could leave the VPN or root daemon running incorrectly, fail to stop it, or route traffic through an unintended runtime; that can cause an outage or security-relevant traffic exposure. Reverting restores the previous code, but it may not immediately undo an already-started daemon, VPN session, or traffic impact.

Blocking findings: runtime/service/src/runtime/service/RuntimeCoordinator.kt:169, runtime/service/src/runtime/service/RuntimeCoordinator.kt:321


Sourcery is free for open source - if you like our reviews please consider sharing them ✨


internal fun onVpnServiceDestroyed(service: TunService) {
if (vpnService === service) vpnService = null
vpnDestroy?.complete(Unit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): A late onVpnServiceDestroyed callback from an old TunService can complete the vpnDestroy deferred belonging to a newer service because the callback completes vpnDestroy before checking the service identity, and stopVpnLocked stores only an unassociated deferred. The coordinator then stops waiting early and can proceed while the current VPN service is still being destroyed.

Triggers: When a VPN service destruction exceeds the 3-second timeout and a replacement service is started before the old onDestroy callback arrives.

Suggested fix: Associate the destruction deferred with its expected service and complete it only when service === expectedService.

Comment on lines +320 to +321
val service = vpnService ?: awaitVpnService()
activeVpn = service

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): startVpnLocked reuses any non-null vpnService without verifying that the instance is still alive. After stopVpnLocked times out waiting for destruction, vpnService remains set until the delayed callback, so a new start can call session.start on the old service instance while its teardown is still pending.

Triggers: When TunService.onDestroy is delayed beyond VPN_DESTROY_TIMEOUT_MS and another start follows immediately.

Suggested fix: Clear or invalidate vpnService before allowing a replacement start, and await or explicitly reject starts while the previous service is still being destroyed.

Replace the launcher/status-store/broadcast protocol with a single
in-process coordinator that serializes start, stop, reload and verify
behind one mutex and publishes a StateFlow<RuntimeState>.

- UI, tile, auto-restart, Wi-Fi automation and config reloads all call
  the coordinator; the MMKV phase slot, session token and lifecycle
  broadcasts are gone.
- Readiness probes GET /group with 25/50/100ms backoff instead of full
  /proxies queries every 200ms; group names are parsed only on demand.
- Drop dead work on the start path: config fingerprints, the unused
  post-start snapshot refresh, the service log stream, no-op observers
  and the transport fallback compile.
- VPN stop waits for TunService.onDestroy and the core pid; connection
  tracking polls only while the app is in the foreground.
@YumeYuka
YumeYuka force-pushed the refactor-simplify-runtime-start-stop branch from f501e8b to bd14377 Compare September 27, 2026 12:09
@YumeYuka
YumeYuka added this pull request to the merge queue Sep 27, 2026
Merged via the queue into Moe with commit af50c17 Sep 27, 2026
7 of 8 checks passed
@YumeYuka
YumeYuka deleted the refactor-simplify-runtime-start-stop branch September 27, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants