Skip to content

fix(desktop): verified update teardown on Windows+WSL, install integrity check, and resilient WSL readiness handshake - #5998

Open
Jgratton24 wants to merge 8 commits into
pingdotgg:mainfrom
Jgratton24:fix/update-apply-wsl-handshake
Open

fix(desktop): verified update teardown on Windows+WSL, install integrity check, and resilient WSL readiness handshake#5998
Jgratton24 wants to merge 8 commits into
pingdotgg:mainfrom
Jgratton24:fix/update-apply-wsl-handshake

Conversation

@Jgratton24

@Jgratton24 Jgratton24 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What Changed

Two Windows+WSL reliability fixes that share one user-visible symptom (an indefinite "Connecting to WSL…" splash), plus a guard that turns the corrupted-install crash into an actionable dialog. Desktop-only; no apps/server changes.

1. Update apply is verified before the installer runs (fixes the half-applied-update corruption).

  • DesktopBackendInstance.stop() now reports whether the child process actually finalized within the timeout instead of silently swallowing it (DesktopBackendManager.ts).
  • The install path (DesktopUpdates.ts) aborts — restarting backends and surfacing the error through the update state machine — if any backend can't be verified stopped, instead of handing off to NSIS while a process still holds files under the install dir.
  • For WSL backends, a stopped wsl.exe relay does not mean the Linux-side server (or helpers it spawned, e.g. cloudflared) exited with it. New ensureWindowsPathReleased (DesktopWslEnvironment.ts) terminates and verifies from inside the distro that no Linux process still holds cwd/exe/fd/mmap references under /mnt/c/<install dir> — 9p/DrvFs handles held from WSL block file replacement on the Windows side, which is how app.asar and app.asar.unpacked end up from different builds.
  • Post-apply integrity check: the build stamps apps/server/dist/desktop-build-manifest.json (under app.asar.unpacked on Windows); startup compares it against the asar's version and refuses to launch a mixed-build install with a clear repair dialog (DesktopInstallIntegrity.ts) instead of the opaque ERR_MODULE_NOT_FOUND main-process crash. Missing manifest (dev, macOS/Linux, pre-stamp installs) skips the check, so no false positives.

2. WSL endpoint detection and the readiness handshake are no longer single-point-of-failure.

  • Networking mode now comes from wslinfo --networking-mode (first-party, present on WSL 2.6.x and 2.7.x) instead of being inferred from the first address in hostname -I — whose ordering shifts whenever Tailscale, Docker bridges, or VPN adapters exist inside the distro. Heuristic fallback for older WSL matches any distro IP against Windows interfaces, never position one, and CGNAT 100.64/10 addresses are never advertised.
  • Readiness probes race loopback plus every enumerated distro address; the first candidate that answers wins and becomes the advertised base URL — so a wrong guess degrades startup by nothing worse than a probe race.
  • After all static candidates time out, the backend's persisted server-runtime.json origin is consulted as a last-resort candidate (gated on it naming this instance's port).
  • The readiness probe is no longer one-shot: a run that spawns but never answers is terminated and retried, and after 3 readiness timeouts the failure surfaces like an exhausted preflight (dialog + Windows fallback on the primary, inline Connections error on the secondary) with every probed URL named. Resolved mode and candidates are logged at startup.

25 new tests across DesktopBackendManager, DesktopUpdates, DesktopWslEnvironment, DesktopBackendConfiguration, and DesktopInstallIntegrity cover the regressions above (CGNAT-first ordering, docker0-first, wslinfo absent, one-shot-probe limbo, unverified stop, WSL busy at install time, mixed-build detection). Full desktop suite: 473 passing.

Why

Two field incidents on 0.0.33-nightly builds, plus these open issues:

The half-applied update we debugged: an in-app update left app.asar + the exe at the old build while app.asar.unpacked/node_modules was new → ERR_MODULE_NOT_FOUND on every launch, with the updater having reported nothing. Root cause: install teardown killed the wsl.exe relay and proceeded on a 5s timeout that was silently swallowed, while the Linux-side server exited asynchronously still holding 9p handles into the install dir. Same version installed fine with the app closed.

Sizing note: aware of the contribution guidelines — roughly half of this diff is tests, and the two fixes share the same touched files (DesktopBackendManager/DesktopWslEnvironment), which is why they're one PR. Happy to split into updater-atomicity and handshake PRs if you'd prefer.

This PR deliberately avoids every file touched by #2829 (orchestrator v2) — verified against its full file list, so it merges cleanly before or after.

UI Changes

No renderer UI changes. Two native dialogs are added/extended (update-install-aborted error via the existing update state machine; "installation is damaged" message box with an Open Download Page button). Not screenshotted — they require a packaged Windows build to trigger, which this dev environment can't produce; dialog copy is in DesktopInstallIntegrity.ts and DesktopUpdates.ts.

Checklist

  • This PR is small and focused (two related reliability fixes, no feature work; ~half the diff is tests)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes (native dialogs only; see UI Changes)
  • I included a video for animation/interaction changes (n/a)

🤖 Generated with Claude Code


Note

High Risk
Touches critical desktop paths: startup gate, update install handoff, backend lifecycle (stop API change), and WSL endpoint selection—bugs could block launch, block updates, or misroute the renderer.

Overview
Hardens Windows desktop startup and updates against half-applied installs and the “connecting to WSL forever” failure mode.

Install integrity at boot: Packaged Windows builds now compare the asar app version to a stamped desktop-build-manifest.json under app.asar.unpacked. A mismatch or missing stamp (when expected) blocks bootstrap with a repair dialog (optional releases link) instead of an opaque main-process module crash. The build pipeline writes that manifest into staged server dist.

Safer in-app updates: Before quitAndInstall, every pooled backend must confirm shutdown within a longer timeout; stop() now returns whether the child actually finalized. Install aborts if any backend is still alive, restarts only backends that were running, and surfaces an install error while keeping the downloaded update. On Windows with WSL backends, a new in-distro check kills/verifies that nothing under /mnt still holds the install directory.

WSL networking and readiness: Single-IP getDistroIp is replaced by all IPv4s from hostname -I, wslinfo --networking-mode, CGNAT filtering, and smarter mirrored vs NAT host choice. Readiness races loopback plus every candidate URL, can fall back to server-runtime.json, rebinds the advertised base URL to whichever probe wins, terminates stuck runs, and caps repeated readiness timeouts / never-ready exits via existing preflight surfacing. Primary pool handling avoids infinite restart when the Windows primary exhausts readiness retries.

Reviewed by Cursor Bugbot for commit 0fdb80d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add install integrity check, verified backend teardown, and resilient WSL readiness probing on Windows

  • Adds a pre-startup install integrity check (DesktopInstallIntegrity.ts) that compares the packaged app version against a desktop-build-manifest.json stamped into app.asar.unpacked at build time; on mismatch or missing manifest (Windows only), shows an error dialog and aborts startup.
  • Replaces best-effort backend stop before updates with a verified 10s-per-backend shutdown; if any backend does not finalize, the install is aborted, previously running backends are restarted, and state is set to failure.
  • On Windows, after verified backend stop, calls ensureWindowsPathReleased to confirm WSL processes have released the install directory before invoking quitAndInstall; aborts if the result is busy or unknown.
  • WSL backend resolution now probes multiple candidate URLs (loopback + all distro IPs via getDistroIps) for readiness in parallel, avoids CGNAT addresses, and falls back to persisted server runtime state when all static candidates time out.
  • DesktopBackendInstance.stop now returns boolean (true = process finalized before timeout); readiness and exit-before-ready failures are bounded by caps and surfaced via onPreflightFailed rather than looping indefinitely.
  • Risk: DesktopWslEnvironment.getDistroIp is removed and replaced by getDistroIps; any code depending on the old single-Option API will break.

Macroscope summarized 0fdb80d.

…iness handshake

Two user-facing failures shared one splash-hang symptom; both are fixed here.

Update apply (half-applied install):
- instance.stop() now reports whether the child fully finalized; the
  install path aborts (and restarts backends, surfacing the error through
  the update state machine) instead of running NSIS while a backend that
  holds install-dir handles may still be alive.
- Before handing off to the installer on Windows, verify from inside the
  distro that no Linux process still holds cwd/exe/fd/mmap references
  under /mnt/c/<install dir> (ensureWindowsPathReleased: TERM, then KILL,
  then report busy). 9p handles held from WSL block file replacement on
  the Windows side, which is how app.asar and app.asar.unpacked ended up
  from different builds.
- Post-apply integrity check: the build stamps
  apps/server/dist/desktop-build-manifest.json (asar-unpacked on Windows);
  startup compares it against the asar version and refuses to launch a
  half-applied install with an actionable repair dialog instead of an
  opaque ERR_MODULE_NOT_FOUND crash.

WSL readiness handshake (indefinite 'Connecting to WSL'):
- Networking mode now comes from wslinfo --networking-mode instead of
  inferring it from the FIRST hostname -I address (a WSL-side tailscaled
  putting a CGNAT 100.64/10 address first made mirrored hosts look NAT'd
  and pointed every probe at an unroutable IP).
- Readiness races loopback and every enumerated distro address; the first
  candidate that answers wins and becomes the advertised base URL.
- After all static candidates time out, the server's persisted
  server-runtime.json origin is consulted as a last-resort candidate.
- A run that spawns but never answers any probe is terminated and retried;
  after 3 readiness timeouts the failure surfaces like an exhausted
  preflight (dialog + Windows fallback on the primary, inline error on the
  secondary) with the probed URLs named, instead of sitting on the splash
  forever. Resolved mode and candidates are logged at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0499cc1d-46bb-4c9a-be82-12db87d581b8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 10, 2026

@macroscopeapp macroscopeapp Bot 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.

Effect service conventions review: one convention violation found — the new readiness fallback path uses Effect.catchTag, which the conventions replace with Effect.catchTags({ ... }) even when recovering a single tag. Everything else (subpath namespace imports, service-member additions on DesktopWslEnvironment with matching layerTest stubs, environment-based dependency acquisition in DesktopUpdates.make, Schema.TaggedErrorClass attributes with an attribute-derived message) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated
Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts Outdated
Comment thread apps/desktop/src/updates/DesktopUpdates.ts
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
Comment thread apps/desktop/src/updates/DesktopUpdates.ts
Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f49d4ea5f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/app/DesktopInstallIntegrity.ts Outdated
Comment thread apps/desktop/src/updates/DesktopUpdates.ts Outdated
Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
Comment thread apps/desktop/src/updates/DesktopUpdates.ts
@macroscopeapp

macroscopeapp Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces significant new runtime behavior across startup (install integrity check), backend lifecycle (readiness probe racing, failure caps), and update installation (verified teardown). Despite good test coverage, the scope of changes to critical desktop infrastructure warrants human review.

You can customize Macroscope's approvability policy. Learn more.

Jgratton24 and others added 2 commits August 10, 2026 08:34
- Recover known tags with Effect.catchTags (convention) in the readiness
  fallback path.
- Anchor the install-dir holder scan: descendants match "$dir/" and the
  directory itself matches as an exact readlink line, so /mnt/c/application
  is no longer classified as a holder of /mnt/c/app.
- Split the release-check result: "unavailable" (wsl.exe would not spawn —
  no running VM can hold /mnt handles) proceeds with a warning, while
  "unknown" (timeout/path-translation failure against a distro that was
  hosting this backend moments ago) now aborts the install.
- abortInstall restarts only instances that were running before teardown,
  so a previously-stopped backend is not resurrected by a failed attempt.
- readServerRuntimeState reads the state-dir flavor the spawned backend
  actually uses (dev/ for --dev-url runs, userdata/ for packaged) instead
  of preferring a possibly-stale packaged file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ess surfacing

- A packaged Windows build with NO unpacked build manifest is now treated
  as a half-applied update (new app.asar over a pre-stamp unpacked tree)
  instead of skipping the check — every Windows artifact that ships the
  checker also ships the stamp. Platforms that pack the server dist inside
  the asar still skip.
- A non-fatal surfacing on a primary that is already the Windows backend
  (only reachable via readiness exhaustion) now shows one dialog and stops
  instead of applying a no-op WSL fallback and looping dialog + restart
  forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/desktop/src/app/DesktopInstallIntegrity.ts Outdated
Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts Outdated
Jgratton24 and others added 2 commits August 10, 2026 08:48
…WSL release always aborts

- The integrity check now distinguishes read failures: only a NotFound on
  packaged Windows is treated as a half-applied update. An unreadable
  manifest (access denied, I/O error) logs and skips instead of falsely
  bricking a healthy install.
- Drop the "unavailable" proceed path from the WSL release check: a
  release-script spawn failure happens after wslpath already succeeded
  against the distro, so it cannot prove WSL is gone — every unverified
  outcome now reads "unknown" and aborts the install. A machine whose WSL
  layer is genuinely broken recovers on the next launch via the preflight
  Windows fallback (no WSL config in the pool means no release check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nstead of restarting forever

Ports the still-relevant piece of pingdotgg#3623: a backend that spawns fine but
keeps exiting before it ever answers a readiness probe previously
restarted silently forever (500ms-10s backoff, no cap, no surfacing).
After 5 consecutive never-ready exits the failure now surfaces through
onPreflightFailed — dialog + Windows fallback on the primary, inline
Connections error on the WSL secondary — exactly like exhausted preflight
and readiness failures.

The counter is scoped strictly to post-spawn exits: pre-spawn preflight
retries have their own counter, a crash after the backend was ready
resets the streak (an established backend crashing is a different failure
than one that never comes up), and the budget refreshes on ready, on
external stop, and on a fresh manual start so one cap firing does not
permanently remove the retry budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 10, 2026
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts Outdated

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 02343e8. Configure here.

Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
…r-ready exit cap

A readiness-timeout termination closes the run scope, and the resulting
child exit previously ALSO incremented exitFailureAttempt — every
readiness timeout double-counted as a crash, so after a readiness-cap
recovery the leftover streak could fire a second, misleading "exited N
times" dialog just two real failures later. The readiness kill now marks
the run stopRequested before closing its scope, which both excludes it
from the exit cap and routes finalize to discardSession (the failure log
was already captured via persistFailureSnapshot). Regression test drives
five readiness cycles with a retrying onPreflightFailed and asserts the
exit cap never fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Jgratton24
Jgratton24 force-pushed the fix/update-apply-wsl-handshake branch from 8e4c156 to 61c09f0 Compare August 10, 2026 13:54
Comment thread apps/desktop/src/backend/DesktopBackendPool.ts Outdated
Jgratton24 and others added 2 commits August 10, 2026 10:06
…SL-primary condition

resolvePrimary only selects the WSL backend when BOTH wslOnly and
wslBackendEnabled are set (wslRequested), but the non-fatal surfacing
branch checked wslOnly alone. With wslOnly persisted true while
wslBackendEnabled is false, a Windows primary exhausting readiness
retries took the WSL branch: a misleading "WSL backend is still
unavailable" dialog, an unrelated in-memory WSL settings mutation, and
one wasted retry cycle before the stop branch finally fired. The guard
now mirrors resolvePrimary's own condition, so a Windows primary
surfaces the generic backend-unavailable dialog and stops immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live testing on a real Windows+WSL host (WSL 2.7, mirrored) showed that
plain Linux fds and mmaps through 9p/DrvFs do NOT block Windows-side file
replacement, while a Windows-side handle does (the backend child runs as
the app executable and reads through app.asar). Comments now describe the
WSL release check as the defensive guarantee it is, with the verified stop
of Windows backend children as the confirmed-mechanism fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

1 participant