Skip to content

fix(runtime): manage daemon lifecycle during launch/reap - #188

Merged
YumeYuka merged 1 commit into
refactortract-animations-rename-varsfrom
fix-daemon-lifecycle-launch-reap
Sep 27, 2026
Merged

YumeYuka merged 1 commit into
refactortract-animations-rename-varsfrom
fix-daemon-lifecycle-launch-reap

Conversation

@YumeLira

@YumeLira YumeLira commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Ensure runtime core replacement, failure recovery, and daemon shutdown are serialized so only one active core owns the tunnel and runtime resources.

Bug Fixes:

  • Prevent runtime launch races by reaping existing VPN, root, and preview cores before starting replacements.
  • Preserve the active root daemon and runtime state when a same-mode replacement fails before the daemon swap.
  • Improve root daemon identity checks and shutdown handling to avoid stale or reused process records.

Enhancements:

  • Serialize core lifecycle operations and coordinate VPN tunnel creation with daemon replacement.
  • Optimize the native loader and payload extraction path for smaller binaries and more efficient I/O.

Build:

  • Tune native loader and liblzma compilation and linking for size reduction and dead-code elimination.

@sourcery-ai

sourcery-ai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR redesigns daemon launch, replacement, and teardown around a shared lifecycle lock with verified process reaping, allowing root reloads to keep serving until the swap while preventing VPN/preview resource races. It also hardens root identity checks and trims the native loader through linker/compiler optimization and reduced decompression/file-sync overhead.

Sequence diagram for serialized runtime core replacement

sequenceDiagram
    participant Launcher as RootSessionLauncher
    participant Core as CoreProcess
    participant Probe as RootDaemonProbe
    participant Daemon as ExistingRootDaemon
    participant Config as CompiledConfigPipeline
    participant NewDaemon as NewCore

    Launcher->>Config: compileDetailed(spec)
    Launcher->>Core: startRoot(mode, finalYaml)
    Core->>Core: withLifecycleLock
    Core->>Probe: reap(record)
    Probe->>Daemon: kill-TERM / kill-KILL
    Probe-->>Core: verified process exited
    Core->>NewDaemon: launchRoot(mode, config)
    NewDaemon-->>Core: CoreEndpoint
    Core-->>Launcher: CoreEndpoint
    Launcher->>Launcher: awaitControllerReady()

    alt compile or launch fails before swap
        Launcher->>Probe: isTrackedRootProcessAlive()
        Probe-->>Launcher: existing daemon still alive
        Launcher-->>Launcher: markRuntimeRunning(servingMode)
    end
Loading

Sequence diagram for VPN launch and daemon reaping

sequenceDiagram
    participant VPN as VpnTunTransport
    participant Core as CoreProcess
    participant Preview as PreviewCoreProcess
    participant Root as RootDaemonProbe
    participant Tunnel as Android VPN
    participant Child as VPN Core

    VPN->>Core: startVpn(config, stack, openTunnel)
    Core->>Core: withLifecycleLock
    Core->>Preview: stopActive()
    Core->>Core: stop and verify existing VPN child
    Core->>Root: reapRootDaemon()
    Root-->>Core: verified root daemon exited
    Core->>Tunnel: openTunnel(config)
    Tunnel-->>Core: VpnTunnel(fd, gateway, dns)
    Core->>Child: launchVpn(fd, gateway, dns, config, stack)
    Child-->>Core: CoreEndpoint
    Core-->>VPN: CoreEndpoint
    VPN->>VPN: releaseReapedHost(vpnService, rootMode)
Loading

State diagram for runtime ownership during root replacement

stateDiagram-v2
    [*] --> RootServing
    RootServing --> CompilingReplacement: compileDetailed(spec)
    CompilingReplacement --> RootServing: compilation fails
    CompilingReplacement --> ReapingOldRoot: startRoot()
    ReapingOldRoot --> LaunchingNewRoot: reap(record) verified
    LaunchingNewRoot --> RootServing: controller ready
    LaunchingNewRoot --> Idle: launch fails after old root is gone
    RootServing --> ReapingOldRoot: VPN or cross-mode launch
    ReapingOldRoot --> LaunchingVpn: root daemon reaped
    LaunchingVpn --> VpnServing: startVpn() succeeds
    ReapingOldRoot --> RootServing: replacement fails before swap
Loading

File-Level Changes

Change Details Files
Serializes all core lifecycle transitions and enforces reaping before replacement launches.
  • Adds a shared lifecycle lock around root, VPN, and preview start/stop operations.
  • Stops and verifies VPN, preview, and root processes before creating a replacement, with bounded TERM/KILL waits.
  • Adds root PID/executable/start-time validation and cached liveness probing to avoid PID reuse errors.
runtime/service/src/runtime/service/core/CoreProcess.kt
runtime/service/src/runtime/service/core/PreviewCoreProcess.kt
runtime/service/src/runtime/service/core/RootDaemonProbe.kt
runtime/service/src/runtime/service/core/AndroidProcessController.kt
Preserves the serving root daemon during same-owner reloads until the replacement is ready.
  • Compiles replacement configuration while the existing root daemon continues serving.
  • Skips the pre-launch stop for root-to-root replacement and restores a live-root snapshot when launch fails before the swap.
  • Releases foreground/status state when a VPN launch reaps the root daemon.
runtime/client/src/runtime/client/session/RuntimeOwnership.kt
runtime/client/src/runtime/client/session/RuntimeSession.kt
runtime/service/src/runtime/service/session/RootSessionLauncher.kt
runtime/service/src/runtime/service/session/VpnTunTransport.kt
Makes preview cores participate safely in the shared runtime lifecycle.
  • Prevents preview startup while a real core owns the runtime resources.
  • Reaps the active preview process before real-core launch and cleans up failed handoffs with bounded waits.
runtime/service/src/runtime/service/core/PreviewCoreProcess.kt
runtime/service/src/runtime/service/core/CoreProcess.kt
Reduces the native loader footprint and avoids redundant I/O work.
  • Enables function/data sectioning, hidden visibility, size optimization, stripping, and linker garbage collection/ICF for the loader and liblzma.
  • Requests sequential file access, ignores redundant XZ container checks, and uses fdatasync for payload durability.
pack/native/CMakeLists.txt
pack/native/loader.c

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 3 issues

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

## Individual Comments

### Comment 1
<location path="runtime/service/src/runtime/service/core/CoreProcess.kt" line_range="597-604" />
<code_context>
-                } finally {
-                    dyingRootPid = null
-                }
+        internal inline fun <T> withLifecycleLock(body: () -> T): T {
+            lifecycleLock.lock()
+            try {
+                return body()
+            } finally {
+                lifecycleLock.unlock()
             }
         }

</code_context>
<issue_to_address>
**issue (bug_risk):** The internal inline `withLifecycleLock` accesses the private `lifecycleLock` from its inlined body, which Kotlin rejects as a non-public API access for an inline function, so the service module fails to compile.

**Suggested fix:** Remove `inline`, or expose the lock through an appropriate `@PublishedApi internal` declaration.

```suggestion
        internal fun <T> withLifecycleLock(body: () -> T): T {
            lifecycleLock.lock()
            try {
                return body()
            } finally {
                lifecycleLock.unlock()
            }
        }
```
</issue_to_address>

### Comment 2
<location path="runtime/service/src/runtime/service/core/RootDaemonProbe.kt" line_range="32-35" />
<code_context>
+ * lock and the controller endpoint.
+ */
+internal object RootDaemonProbe {
+    fun trackedAlive(): Boolean {
+        val pid = RootDaemonState.load()?.pid ?: return false
+        if (pid <= 0) return false
+        return exec("kill -0 $pid")?.isSuccess == true
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Root liveness and reaping use only the recorded PID, and `reap` does not compare the recorded start time before signaling a matching executable. When the daemon PID is recycled, the code treats an unrelated process as the daemon and can terminate it, or reports a stale daemon as still serving.

**Triggers:** When the persisted root PID has been reused after the original daemon exits.

**Suggested fix:** Require executable and start-time identity validation in `trackedAlive` and before `reap` sends signals; discard a record on a definite identity mismatch.
</issue_to_address>

### Comment 3
<location path="runtime/service/src/runtime/service/core/PreviewCoreProcess.kt" line_range="90" />
<code_context>
         controller = null
         if (previous != null) {
-            runCatching { previous.terminate() }
+            reap(previous)
         }
     }
</code_context>
<issue_to_address>
**issue (bug_risk):** `reap` ignores the result of both exit waits, so `stopLocked` returns even when the preview process remains alive after SIGKILL. The subsequent real-core launch then proceeds with the old preview process still running and sharing the runtime home/socket resources.

**Triggers:** When a preview process does not exit within the 300 ms grace period plus the 200 ms kill wait.

**Suggested fix:** Return the reap result and abort the launch, or continue waiting/fail explicitly when the process remains alive.

```suggestion
            reap(previous)
            check(!File("/proc/${previous.pid}").exists()) {
                "preview process remains alive after reap"
            }
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 3 findings to address first, and a failure in the new locking, PID identity checks, or reap paths could leave two privileged cores competing for the tunnel, routes, or controller socket, or terminate the wrong process and cause an outage. Reverting prevents future launches from using this lifecycle, but it cannot undo a networking outage or other teardown side effects that already occurred.

Blocking findings: runtime/service/src/runtime/service/core/CoreProcess.kt:604, runtime/service/src/runtime/service/core/RootDaemonProbe.kt:35, runtime/service/src/runtime/service/core/PreviewCoreProcess.kt:90


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

Comment on lines +597 to 604
internal inline fun <T> withLifecycleLock(body: () -> T): T {
lifecycleLock.lock()
try {
return body()
} finally {
lifecycleLock.unlock()
}
}

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): The internal inline withLifecycleLock accesses the private lifecycleLock from its inlined body, which Kotlin rejects as a non-public API access for an inline function, so the service module fails to compile.

Suggested fix: Remove inline, or expose the lock through an appropriate @PublishedApi internal declaration.

Suggested change
internal inline fun <T> withLifecycleLock(body: () -> T): T {
lifecycleLock.lock()
try {
return body()
} finally {
lifecycleLock.unlock()
}
}
internal fun <T> withLifecycleLock(body: () -> T): T {
lifecycleLock.lock()
try {
return body()
} finally {
lifecycleLock.unlock()
}
}

Comment on lines +32 to +35
fun trackedAlive(): Boolean {
val pid = RootDaemonState.load()?.pid ?: return false
if (pid <= 0) return false
return exec("kill -0 $pid")?.isSuccess == true

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): Root liveness and reaping use only the recorded PID, and reap does not compare the recorded start time before signaling a matching executable. When the daemon PID is recycled, the code treats an unrelated process as the daemon and can terminate it, or reports a stale daemon as still serving.

Triggers: When the persisted root PID has been reused after the original daemon exits.

Suggested fix: Require executable and start-time identity validation in trackedAlive and before reap sends signals; discard a record on a definite identity mismatch.

controller = null
if (previous != null) {
runCatching { previous.terminate() }
reap(previous)

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): reap ignores the result of both exit waits, so stopLocked returns even when the preview process remains alive after SIGKILL. The subsequent real-core launch then proceeds with the old preview process still running and sharing the runtime home/socket resources.

Triggers: When a preview process does not exit within the 300 ms grace period plus the 200 ms kill wait.

Suggested fix: Return the reap result and abort the launch, or continue waiting/fail explicitly when the process remains alive.

Suggested change
reap(previous)
reap(previous)
check(!File("/proc/${previous.pid}").exists()) {
"preview process remains alive after reap"
}

@YumeYuka
YumeYuka force-pushed the fix-daemon-lifecycle-launch-reap branch from ce11f4e to e99c1f6 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 d37d0fa Sep 27, 2026
7 of 8 checks passed
@YumeYuka
YumeYuka deleted the fix-daemon-lifecycle-launch-reap 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