Repository navigation
Retain current and previous Windows runtime generations with process leases - #6181
Merged
Merged
Conversation
…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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #5981 (item 1, approved rule A1). Item 2 (declined or failed msiexec recovery) will follow in a separate PR.
What changed
sidecar_runtime_retentionmodule. It compiles on Windows and in tests on every desktop target, so its tests also run on macOS and Linux. Each app holds an exclusivefs2lock on its own file undersidecar-runtime/.leases/<generation>/for its whole lifetime, and recordslast-active. The OS releases the lock on exit or crash.node.exeis in the kill-on-close job object, so the app's lease covers it too.prepare_leased_runtime_from_fileschecks 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_runtimekeeps the lease in a process-lifetime registry..leasesand 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.cleanup_retains_all_complete_generations_without_process_leaseswithtwo_updates_leave_two_runtime_generationsandcleanup_retains_leased_generations_beyond_the_rollback_copy.windows-startup.ymlnow runssidecar_archive::tests(24) andsidecar_runtime_retention::tests(5) with exact counts. Before this, those tests ran on Windows only in full-validation and release. Therelease-runtimecontract asserts both entries.docs/windows-runtime-cache.mddocuments the rule.Verification
two_updates…,a_leased_generation_survives…,…marker_age). All 5 pass after the implementation.cargo test --manifest-path src-tauri/Cargo.toml --locked --libpassed 309 tests.node --test src-tauri/release-runtime.test.mjspassed 31. Scoped rustfmt is clean; the existingbuild.rsdrift is untouched.sidecar_archivemodule 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.