Skip to content

Retain current and previous Windows runtime generations with process leases - #6181

Merged
BunsDev merged 2 commits into
mainfrom
fix/issue-5981-retention-install
Oct 22, 2026
Merged

BunsDev merged 2 commits into
mainfrom
fix/issue-5981-retention-install

Conversation

@BunsDev

@BunsDev BunsDev commented Oct 22, 2026

Copy link
Copy Markdown
Member

Refs #5981 (item 1, approved rule A1). Item 2 (declined or failed msiexec recovery) will follow in a separate PR.

What changed

  • New sidecar_runtime_retention module. It compiles on Windows and in tests on every desktop target, so its tests also run on macOS and Linux. Each app holds an exclusive fs2 lock on its own file under sidecar-runtime/.leases/<generation>/ for its whole lifetime, and records last-active. The OS releases the lock on exit or crash. node.exe is in the kill-on-close job object, so the app's lease covers it too.
  • prepare_leased_runtime_from_files checks readiness, extracts and leases in one cache-locked scope. The lock-free warm path is gone, so cleanup, which holds the same lock, can't retire a generation between the readiness check and the lease. prepare_sidecar_runtime keeps the lease in a process-lifetime registry.
  • Startup cleanup skips .leases and still removes incomplete generations and stale staging. For complete generations it keeps the current one plus the newest other one as the rollback copy, and retires any other only when every lease file can be locked. Held, unreadable or erroring lease state keeps the generation.
  • I deliberately replaced the pinned test cleanup_retains_all_complete_generations_without_process_leases with two_updates_leave_two_runtime_generations and cleanup_retains_leased_generations_beyond_the_rollback_copy.
  • windows-startup.yml now runs sidecar_archive::tests (24) and sidecar_runtime_retention::tests (5) with exact counts. Before this, those tests ran on Windows only in full-validation and release. The release-runtime contract asserts both entries. docs/windows-runtime-cache.md documents the rule.

Verification

  • Failing-first: against a keep-everything stub, 3 of 5 retention tests failed (two_updates…, a_leased_generation_survives…, …marker_age). All 5 pass after the implementation.
  • macOS: cargo test --manifest-path src-tauri/Cargo.toml --locked --lib passed 309 tests. node --test src-tauri/release-runtime.test.mjs passed 31. Scoped rustfmt is clean; the existing build.rs drift is untouched.
  • The sidecar_archive module is Windows-only and no Windows std is available locally. Its changes compile and run only in this PR's Windows startup lane, which is the evidence to check.
  • Not yet verified on real Windows: two real updates leaving two generations (the Windows runtime cache and updater: cap generations, recover a declined UAC, retry during supervisor cooldown, show release notes #5981 T5 handoff).

…leases

Each running app now leases the content-addressed sidecar generation it
serves. Preparation and leasing share one cache-locked scope, and the app
holds the lease for its whole lifetime. Startup cleanup keeps the current
generation and the most recently activated other generation, and retires
any other complete generation only after proving no live lease holds it.
Unknown lease state retains the generation. This replaces the pinned
retain-everything test deliberately (approved rule A1 on #5981).

Refs #5981

Signed-off-by: Val Alexander 👑 <val@opencoven.ai>
…eparation test

The cache now holds a .leases directory by design. Assert exactly one
generation and that the preparation leased it.

Refs #5981

Signed-off-by: Val Alexander 👑 <val@opencoven.ai>
@BunsDev
BunsDev merged commit 726ef52 into main Oct 22, 2026
23 checks passed
@BunsDev
BunsDev deleted the fix/issue-5981-retention-install branch October 22, 2026 22:43
@github-project-automation github-project-automation Bot moved this from Todo to Done in Cave Oct 22, 2026
BunsDev added a commit that referenced this pull request Oct 23, 2026
Refs #5981 (item 2, approved mode **B1**: stay per-machine and add a watcher). Independent of #6181 (retention). Once one merges, the other will need a trivial rebase in `windows-startup.yml` and `release-runtime.test.mjs`.

## Problem (source-verified)
`tauri-plugin-updater` 2.10.1 `install_inner` runs `on_before_exit` (`cleanup_before_exit`), calls `ShellExecuteW(msiexec /i … /passive … AUTOLAUNCHAPP=True)`, **ignores the result** and calls `std::process::exit(0)`. Tauri CLI 2.11.4's MSI is `InstallScope="perMachine"`, and `LaunchApplication` runs only `After="InstallFinalize"`. So a declined UAC prompt or a failed msiexec left Cave closed. `install()` never returns an error in that case.

## Change
- **Arm:** On Windows, `installPreparedUpdate` calls `arm_update_install_recovery` before `update.install()`. That copies the exe to `%LOCALAPPDATA%\ai.opencoven.cave\update-install-watch\`, outside the install directory. If arming fails, the install doesn't start and the existing failure banner with the release page appears. If `install()` throws, the webview disarms.
- **Watch:** `UpdateInstallWatchGuard` sits in the resource table, so the updater's pre-exit `cleanup_before_exit` (the same drop that stops the sidecar) starts the copied watcher. The watcher identifies the app by PID and creation time and waits up to 120 s for it to exit. It then finds the `msiexec.exe` child, using creation time to rule out a reused PID, and reads its exit code.
- **Outcome:** Codes 0, 1641 and 3010 mean success; nothing happens and the MSI relaunches. 1602 and 1223 are recorded as *cancelled*, any other code as *failed*, and a missing installer as *not started*. The watcher writes `update-install-outcome.json` (a file, not argv, so the MSI's `LAUNCHAPPARGS` can't replay it after a later success) and relaunches Cave.
- **UI:** On mount, the banner reads the outcome through a validating parser and shows a warning with the installer code, the `msi-upgrade-from-*.log` path and **Retry update**. The record clears when the user dismisses or retries, or when a new install is armed.
- Signed download verification and explicit **Restart & install** consent are unchanged. New commands have permission entries. `windows_update_recovery::tests` (5) is added to the Windows PR lane with an exact count. Docs: `docs/windows-update-preparation.md`.

## Verification (local, macOS)
- `cargo test --manifest-path src-tauri/Cargo.toml --locked --lib`: 309 passed, including 5 new tests (exit classification, the argument contract, outcome persistence, and serialization for the webview).
- `pnpm typecheck` ✓. `pnpm lint` ✓. `pnpm check:tests-wired`: 2129 ✓. `pnpm test:app`: exit 0. `node --test src-tauri/release-runtime.test.mjs src-tauri/permissions/desktop-permissions.test.mjs`: 49 ✓.
- New behavior cases: Retry and dismissal on the outcome banner, arm → install → disarm ordering, and refusal when arming fails. New `app-update` parser and copy cases. Pinned update-available source assertions updated deliberately: `install()` is still the first update action after the arm step.
- **Not verified locally:** the Win32 watcher (Toolhelp snapshot, `GetProcessTimes`, waits) compiles only in this PR's Windows lane, since no Windows std is available here. On real Windows (T5), still to confirm: declining UAC relaunches a working app with this banner, and the client `msiexec.exe` exit code on decline (expected 1602 or 1625).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant