Skip to content

feat(windows): add native daemon lifecycle and control - #1363

Open
joshuajbouw wants to merge 48 commits into
mainfrom
feat/1350-windows-daemon-lifecycle
Open

feat(windows): add native daemon lifecycle and control#1363
joshuajbouw wants to merge 48 commits into
mainfrom
feat/1350-windows-daemon-lifecycle

Conversation

@joshuajbouw

@joshuajbouw joshuajbouw commented Jul 24, 2026

Copy link
Copy Markdown
Member

Linked Issue

Closes #1350

Summary

Adds native Windows daemon lifecycle and control while preserving the existing
Unix behavior and persistent-daemon default. Windows startup now reaches the
real kernel composition root over the authenticated per-user named-pipe
transport, and lifecycle decisions are based on authenticated readiness and
verified process identity rather than filesystem socket assumptions.

Changes

  • Add native Windows background/foreground process creation, exact-process
    stop escalation, restart, status, doctor, and .exe companion discovery.
  • Return a nonzero CLI status when daemon termination cannot be confirmed so
    automation cannot mistake an unverified stop for success.
  • Treat authenticated shutdown transport errors as graceful-stop failures that
    fall through to exact-process termination while keeping daemon rejections and
    unexpected authenticated responses fail-closed; preserve typed handshake
    rejection through the uplink so process recovery cannot bypass it; re-probe
    transport when no process remains so a lost acknowledgement is confirmed
    without trusting a stale endpoint.
  • Treat a stale Windows PID as absent only when the process API confirms exit;
    access-denied and unexpected liveness results remain fail-closed and are
    never signaled without executable identity. Persist and verify the daemon's
    process-creation timestamp on the same retained process handle so PID reuse
    cannot target a newer process running the same installed image.
  • Transfer detached Unix daemon children to a dedicated child reaper before long-lived CLI modes continue, while Windows closes only the parent-side handle; cleanup failures retain process ownership until a waiter accepts it on both platforms.
  • Prevent detached Windows daemons and update helpers from retaining ambient stdin/stdout/stderr handles inherited from redirected callers; native x64 and ARM64 regression coverage proves redirected streams reach EOF while the detached child remains alive.
  • Keep persistent daemons detached without an implicit idle timeout; ephemeral
    lifetime follows authenticated client presence and foreground mode propagates
    daemon exit.
  • Make the kernel's native composition root, audit initialization, singleton
    lock, readiness/PID/token publication, and graceful cleanup work on Windows;
    shutdown removes the exact boot-home artifacts instead of re-resolving
    process-global state.
  • Keep the existing Unix signal-watchdog implementation target-gated while
    Windows shutdown remains driven by console control and API requests.
  • Add fail-closed Windows path resolution and additive fallible path APIs while
    preserving the existing public wrappers and Unix fallback behavior, and
    apply private Windows ACLs to generated grant directories and files.
  • Add authenticated, transaction-journaled Windows self-update with an
    exact-parent PID/creation-time helper handshake, digest revalidation,
    replacement of the complete daemon/build/emit/CLI executable set, rollback,
    durable result receipts, crash recovery, and non-elevated stale-stage
    cleanup. A provisional handoff cannot be recovered prematurely, while an
    abandoned pre-mutation handoff is removed after the exact parent is confirmed
    gone. Recovery scans the complete live executable set: a missing or unreadable
    member remains durably recovery_pending with the helper, transaction, and
    staged payload retained; only a complete readable set can be terminalized.
    Signed channel continuity state is provisioned with private Windows ACLs and
    uses the recovery-aware private read/write transaction boundary.
  • Persist self-managed Windows installs in the per-user registry PATH only
    when all four release executables are present. Preserve REG_SZ versus
    REG_EXPAND_SZ, compare normalized paths case-insensitively, serialize
    Astrid writers, re-read immediately before mutation, verify after mutation,
    and broadcast the environment change with a bounded synchronous Win32 call.
  • Share exact-handle private file locks between daemon singleton, updater, and
    PATH writers. Open daemon boot diagnostics with true append-only Windows
    access so concurrent startup handles cannot overwrite earlier bytes.
  • Publish x86_64 and ARM64 MSVC release archives plus a signed additive Windows
    metadata extension bound to the legacy four-target manifest, preserving
    compatibility for existing release-channel clients while allowing Windows
    clients to authenticate exact archive digests.
  • Revalidate workspace readiness before reporting an already-running daemon,
    and prove that an authenticated principal without system:shutdown cannot
    stop or replace the daemon process.
  • Add native x86_64 and ARM64 Windows CI covering compilation, Clippy, unit
    regressions, persistent idle survival, and a real authenticated
    start/status/stop/process-exit cycle using an immutable AOS capsule-cli
    uplink fixture installed into a runtime-provisioned private home; hold and
    release an MCP stdio client's named-pipe lease and require the auto-spawned
    ephemeral daemon to exit promptly; preserve daemon boot logs as failure
    artifacts, and keep every CLI probe and cleanup command bounded while
    retaining stdout/stderr diagnostics.
  • Run the real Windows updater lifecycle in an independent native x64/ARM64
    release-profile matrix. It recursively checks out the pinned WIT input,
    builds the exact four executable release set, rejects any artifact above the
    authenticated 100 MiB per-binary bound, and then exercises the real helper,
    transaction, receipt, rollback, and reconciliation paths with unchanged
    operation deadlines.
  • Integrate with the private Windows capsule-home provisioning merged in
    fix(windows): provision capsule install home #1370, so the native lifecycle smoke starts from an absent home and exercises
    the real secure setup path.
  • Provision native kernel lifetime fixtures under an exact-private Windows
    LocalAppData boundary instead of inheriting the hosted runner's broad
    temporary-directory ACL.

Verification

  • cargo fmt --all -- --check
  • git diff --check
  • Rust 1.95 Windows-targeted Clippy with pedantic warnings denied for the shared detached-process module and its tests on x86_64-pc-windows-msvc and aarch64-pc-windows-msvc
  • cargo metadata --no-deps --locked --format-version 1
  • python3 scripts/test_release_manifest.py (16 passed)
  • python3 scripts/test_musl_release_manifest.py (7 passed)
  • python3 scripts/test_windows_release_manifest.py (8 passed)
  • python3 scripts/test_release_publication.py (11 passed)
  • actionlint .github/workflows/windows-daemon-lifecycle.yml .github/workflows/release.yml
  • Native x86_64 and ARM64 Windows Clippy, private-fixture lifecycle suites,
    the full daemon suite, kernel transport/readiness tests, updater
    completion/recovery, persistent PATH, and authenticated process smoke are
    enforced by the dedicated workflows. Debug native jobs compile and lint the
    updater target and run its state-machine regressions; independent
    release-profile jobs build and bound all four shipped executables before
    running the real staged helper and executable. Stale-PID recovery uses a
    confirmed exited native child rather than a synthetic process identifier.
  • Independent adversarial review of lifecycle semantics, Windows kernel boot,
    process identity, exact-path shutdown cleanup, updater rollback and recovery
    classification, signed release metadata and promotion verification, channel
    continuity/ACL handling, release-profile CI fidelity, and public API shape.

Checklist

  • Linked to an issue
  • CHANGELOG.md updated (entry under [Unreleased])

Copilot AI review requested due to automatic review settings July 24, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a native Windows daemon lifecycle/control implementation (start/stop/restart/status/doctor/self-update) while preserving existing Unix behavior and default persistent-daemon semantics. This extends the runtime’s local-transport and on-disk runtime artifacts to work robustly on Windows (named pipes, authenticated readiness, verified process identity), and adds native Windows CI coverage to exercise the real lifecycle.

Changes:

  • Implement Windows-native daemon lifecycle semantics in the CLI (foreground/background spawn, authenticated readiness probing, verified stop escalation, and improved doctor/status behavior).
  • Update kernel/uplink runtime-path handling to fail-closed on Windows (no /tmp fallback) and to use private/atomic runtime artifact writes.
  • Add Windows self-update helper transaction flow plus Windows CI workflow + script to validate start/status/stop and confirmed exit.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/windows-daemon-lifecycle.ps1 Adds a PowerShell lifecycle smoke test used by Windows CI to exercise authenticated start/status/stop.
crates/astrid-uplink/src/socket_client.rs Introduces fallible try_*_path APIs and makes Windows path resolution fail-closed.
crates/astrid-kernel/src/socket.rs Makes runtime artifact paths/windows behavior fail-closed; uses private dirs and atomic private writes on Windows.
crates/astrid-kernel/src/lib.rs Enables the native socket/named-pipe manager on Windows and updates shutdown cleanup semantics.
crates/astrid-daemon/src/main.rs Keeps background Unix setsid behavior but preserves attachment in foreground mode via env marker.
crates/astrid-daemon/src/lib.rs Updates wording/contracts to “local transport” and aligns error messaging with cross-platform uplink semantics.
crates/astrid-cli/src/socket_client.rs Switches CLI re-exports to the new fallible try_* path APIs.
crates/astrid-cli/src/main.rs Adds early Windows self-update helper dispatch before normal CLI parsing.
crates/astrid-cli/src/dispatch.rs Updates start to accept args; adjusts stop call site (currently discards confirmation).
crates/astrid-cli/src/daemon.rs Keeps daemon setsid behavior in background but not in foreground mode.
crates/astrid-cli/src/commands/who.rs Uses fallible local-transport path resolution instead of implicit fallback path logic.
crates/astrid-cli/src/commands/self_update/windows.rs Adds Windows self-update helper/staging/transaction logic (copy, digest recheck, replace, cleanup).
crates/astrid-cli/src/commands/self_update.rs Enables Windows self-update flow and adds Windows platform target mapping and helper entrypoints.
crates/astrid-cli/src/commands/self_update_tests.rs Extends tests for Windows target selection and managed binary naming (but needs cfg fixes for Windows builds).
crates/astrid-cli/src/commands/restart.rs Tightens restart semantics to require confirmed stop and uses PID identity gating before respawn.
crates/astrid-cli/src/commands/ps.rs Uses fallible local-transport path resolution.
crates/astrid-cli/src/commands/mod.rs Adds the new daemon_process helper module.
crates/astrid-cli/src/commands/doctor.rs Moves daemon health inference to authenticated status roundtrip instead of endpoint pathname presence.
crates/astrid-cli/src/commands/daemon.rs Implements authenticated daemon probing/readiness, foreground mode, and stop confirmation plumbing.
crates/astrid-cli/src/commands/daemon_tests.rs Extracts/extends lifecycle unit tests for new start/stop/foreground logic.
crates/astrid-cli/src/commands/daemon_process.rs Adds Windows background process creation flags for detached daemon spawning.
crates/astrid-cli/src/commands/daemon_control/windows.rs Adds Windows process identity verification and termination primitives using handles.
crates/astrid-cli/src/commands/daemon_control.rs Integrates Windows termination + path identity, and validates PID file privacy on Windows.
crates/astrid-cli/src/commands/capsule/live_load.rs Avoids panicking path resolution; treats unresolved transport path as “unreachable”.
crates/astrid-cli/src/cli.rs Updates the Start command to accept StartArgs (help text needs refresh for --foreground).
crates/astrid-cli/src/bootstrap.rs Updates companion binary discovery to use the native executable suffix (contains a formatting compile bug).
crates/astrid-cli/Cargo.toml Adds windows-sys dependency for Windows-only lifecycle/update features.
CHANGELOG.md Adds an [Unreleased] entry describing the Windows daemon lifecycle/control and self-update work.
Cargo.lock Records the added windows-sys dependency.
.github/workflows/windows-daemon-lifecycle.yml Adds Windows x64/ARM64 CI workflow to build, test, and run lifecycle smoke script.
Comments suppressed due to low confidence (1)

crates/astrid-cli/src/commands/self_update_tests.rs:438

  • This test uses backup_and_swap(), but backup_and_swap is compiled only on non-Windows (#[cfg(not(windows))]). As written, Windows test builds will fail to compile. Gate this test to non-Windows (or add an equivalent Windows-path test).
    let extract = dir.path().join("new");
    std::fs::create_dir_all(&install).unwrap();
    std::fs::create_dir_all(&extract).unwrap();
    let cli = MANAGED_BINARIES[0];
    let daemon = MANAGED_BINARIES[1];

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/astrid-cli/src/bootstrap.rs
Comment thread crates/astrid-cli/src/dispatch.rs Outdated
Comment thread crates/astrid-cli/src/cli.rs Outdated
Comment thread crates/astrid-cli/src/commands/self_update_tests.rs Outdated
@joshuajbouw
joshuajbouw force-pushed the feat/1350-windows-daemon-lifecycle branch from 2f84f09 to fb4177b Compare July 25, 2026 00:29
@joshuajbouw
joshuajbouw requested a review from Copilot July 25, 2026 01:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.

Comment thread crates/astrid-cli/src/commands/daemon.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 2 comments.

Comment thread crates/astrid-uplink/src/socket_client.rs
Comment thread scripts/windows-daemon-lifecycle.ps1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 2 comments.

Comment thread crates/astrid-cli/src/commands/daemon_process.rs
Comment thread crates/astrid-cli/src/commands/self_update/windows.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.

Comment thread crates/astrid-cli/src/socket_client.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 2 comments.

Comment thread crates/astrid-cli/src/commands/self_update.rs Outdated
Comment thread crates/astrid-cli/src/commands/self_update/windows.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 2 comments.

Comment thread crates/astrid-cli/src/commands/daemon.rs
Comment thread crates/astrid-kernel/src/lib.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 48 out of 49 changed files in this pull request and generated 2 comments.

Comment thread crates/astrid-kernel/src/lib.rs
Comment thread scripts/musl_release_manifest.py Outdated
@joshuajbouw
joshuajbouw force-pushed the feat/1350-windows-daemon-lifecycle branch from cefbe9c to b3c9c07 Compare July 25, 2026 20:35
@joshuajbouw

Copy link
Copy Markdown
Member Author

@copilot review

@joshuajbouw

Copy link
Copy Markdown
Member Author

@copilot review

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Reviewed the full PR diff, including the latest commit (b3c9c07 "test(windows): secure router test homes"). No issues found.

kernel_router/tests.rs — remaining tempfile::tempdir() migrations

  • The three tests that still used bare tempfile::tempdir() (denied_shutdown_does_not_consume_an_authorized_principals_budget, authorization_denial_does_not_precharge_a_later_grant, device_scope_denial_does_not_consume_the_principals_budget) are now using PrivateRouterTestHome::new().
  • The single tempfile::tempdir() remaining in the file (line 46) is inside the #[cfg(not(windows))] branch of PrivateRouterTestHome::new() — the correct Unix path. All Windows test homes now go through ensure_private_directory under LocalAppData.
  • The PrivateRouterTestHome struct correctly holds onto the TempDir guard on Unix (preventing early cleanup via RAII) and implements Drop on Windows to remove the private test directory.

CI: Changelog, Dependency Review, CodeQL, Analyze Rust, OCI builds, and Format all pass on the latest commit. No failures.

Copilot AI commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Reviewed the full PR diff, including the latest commit (0be5e8f "ci(windows): run management router tests natively"). No new issues found.

windows-daemon-lifecycle.yml — new "Test native management router" step

  • Adds cargo test -p astrid-kernel --locked --target $env:TARGET kernel_router::tests as a native Windows CI step, directly enforcing the b3c9c07 PrivateRouterTestHome migration.
  • Step shape is identical to every other test step in the workflow: pwsh shell, TARGET env from matrix, same flag ordering. No structural inconsistency.
  • Placement (between "Test native daemon runtime" and "Test native transport lifecycle") is logical — all kernel test steps are grouped together.
  • No missing flags: the kernel_router::tests suite has no feature-gated paths that require --all-features, consistent with the other kernel test steps in this workflow.

The PR is in good shape.

@joshuajbouw

Copy link
Copy Markdown
Member Author

@copilot review

Exact head 29c7e1de fixes the ARM64 updater integration failure without changing any deadline: the test now observes the recovery helper stage lock and reads the terminal receipt only after helper release, instead of polling the receipt through the same fail-fast private-file transaction lock required by the helper writer. The completed invoking child handle is also closed before recovery completion is asserted. Please review the full current diff.

Copilot AI commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Exact head 29c7e1de fixes the ARM64 updater integration failure without changing any deadline: the test now observes the recovery helper stage lock and reads the terminal receipt on...

Reviewed the full PR diff, including commit 29c7e1d ("test(windows): avoid updater receipt lock contention"). No new issues found.

Root-cause analysis
read_private_file_to_string acquires acquire_private_file_transaction_lock (a per-staging-directory exclusive lock) before reading. The recovery helper holds that same transaction lock while writing the receipt via atomic_write_private_file. Polling the receipt from the test therefore contended directly with the helper's terminal write, causing the ARM64 runner to lose the window.

Fix correctness
The stage lock (helper.lock) is a separate exclusive handle held by the recovery helper for its entire run and dropped in report_terminal_receipt only after atomic_write_private_file completes (line 732 of windows.rs). Observing that lock instead of polling the receipt provides a strictly-after ordering guarantee: by the time wait_for_update_helper_release returns, the receipt is fully committed and the subsequent single read_receipt_state call is contention-free.

wait_for_update_helper_release implementation

  • Ok(Some(_)) → lock acquired (helper gone), drop immediately and return — correct.
  • Ok(None) → lock still held, continue polling — correct.
  • Err(_) → unexpected path error, panic with staging snapshot — correct (fails closed).
  • Timeout → panic with same staging snapshot — consistent with other timeout helpers in this file.

drop(blocked) addition
ChildGuard::Drop calls terminate_child_bounded_sync. Dropping after wait_for_child returns (process already exited and waited) is safe; the explicit drop makes the handle lifetime clear before the lock-observation phase begins.

Scope
wait_for_receipt_state is now unused; it was replaced rather than retained as dead code — correct cleanup.

The PR is in good shape.

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.

Implement native Windows daemon lifecycle and control

3 participants