Windows auto-update: NSIS "Error opening file for writing: cinny.exe" — old process still holds the exe #1

Closed
opened 2026-09-15 08:47:06 -04:00 by jared · 3 comments
Owner

Symptom (Windows, last two auto-updates): the NSIS installer window pops up with

Error opening file for writing: C:\Users\<user>\AppData\Local\Lotus Chat\cinny.exe — Abort / Retry / Ignore

Clicking Ignore skips replacing the exe, so the app silently stays on the old version and the same update is offered again next time.

Immediate workaround: click Retry instead of Ignore. By then the old process has exited and the write succeeds.

Analysis

install_update (src-tauri/src/lib.rs) calls update.download_and_install(...). On Windows, tauri-plugin-updater 2.10.1 spawns the NSIS installer with /P /R (passive + relaunch), runs the on_before_exit hook, then std::process::exit(0) (updater.rs ~L838-865). The installer therefore starts while the old cinny.exe is still alive and immediately tries to overwrite it; if the process (WebView2 teardown, tray, single-instance) hasn't fully released the file yet, NSIS hits the lock. Our app.restart() after the install call is never reached on Windows (the plugin has already exited), so it isn't the cause, but it is dead code on that platform.

Candidate fixes (need a Windows build to verify)

  1. Register an on_before_exit hook via app.updater_builder().on_before_exit(...) that closes all webview windows and removes the tray icon so the exe handle is released before NSIS runs, and check whether the bundled NSIS template's running-app check (nsis_tauri_utils::FindProcess / kill in update mode) is actually active for /P passive installs in this Tauri version.
  2. If that isn't enough: update.download() to a temp file ourselves and launch the installer detached via cmd /c "timeout /t 2 >nul && start "" "<installer>" /P /R" before app.exit(0), giving the process time to die. Uglier, but deterministic.
  3. Alternatively switch plugins.updater.windows.installMode to quiet (/S /R) and confirm whether the silent path force-closes the running app (Tauri's template behaves differently in silent vs passive mode).

Related: the "update available" state losing on Settings close is fixed in cinny (useTauriUpdater now keeps it in a shared atom).

**Symptom (Windows, last two auto-updates):** the NSIS installer window pops up with > Error opening file for writing: `C:\Users\<user>\AppData\Local\Lotus Chat\cinny.exe` — Abort / Retry / Ignore Clicking **Ignore** skips replacing the exe, so the app silently stays on the old version and the same update is offered again next time. **Immediate workaround:** click **Retry** instead of Ignore. By then the old process has exited and the write succeeds. ### Analysis `install_update` (src-tauri/src/lib.rs) calls `update.download_and_install(...)`. On Windows, `tauri-plugin-updater` 2.10.1 spawns the NSIS installer with `/P /R` (passive + relaunch), runs the `on_before_exit` hook, then `std::process::exit(0)` (updater.rs ~L838-865). The installer therefore starts *while the old `cinny.exe` is still alive* and immediately tries to overwrite it; if the process (WebView2 teardown, tray, single-instance) hasn't fully released the file yet, NSIS hits the lock. Our `app.restart()` after the install call is never reached on Windows (the plugin has already exited), so it isn't the cause, but it is dead code on that platform. ### Candidate fixes (need a Windows build to verify) 1. Register an `on_before_exit` hook via `app.updater_builder().on_before_exit(...)` that closes all webview windows and removes the tray icon so the exe handle is released before NSIS runs, and check whether the bundled NSIS template's running-app check (`nsis_tauri_utils::FindProcess` / kill in update mode) is actually active for `/P` passive installs in this Tauri version. 2. If that isn't enough: `update.download()` to a temp file ourselves and launch the installer detached via `cmd /c "timeout /t 2 >nul && start "" "<installer>" /P /R"` before `app.exit(0)`, giving the process time to die. Uglier, but deterministic. 3. Alternatively switch `plugins.updater.windows.installMode` to `quiet` (`/S /R`) and confirm whether the silent path force-closes the running app (Tauri's template behaves differently in silent vs passive mode). Related: the "update available" state losing on Settings close is fixed in cinny (`useTauriUpdater` now keeps it in a shared atom).
jared self-assigned this 2026-09-15 08:47:06 -04:00
Author
Owner

Fix shipped in 4648c8c: an NSIS pre-install hook that, on /UPDATE only, polls until cinny.exe can actually be opened for writing (every 250 ms, up to 15 s) before the template copies files.

Tested under Wine with a real running exe:

  • without the hook the copy fails (the bug);
  • with it, the installer waits out the remaining ~2.8 s, then copies;
  • a manual install (no /UPDATE) doesn't wait;
  • a stuck process hits the 15 s cap and falls through to the template's own running-app handling.

build-windows compiled it (run 2057). Leaving open until a real auto-update onto a build with this hook completes without the "Error opening file for writing" dialog. The installer that runs is the new version's, so the first update onto this build is already the test.

Fix shipped in `4648c8c`: an NSIS pre-install hook that, **on `/UPDATE` only**, polls until `cinny.exe` can actually be opened for writing (every 250 ms, up to 15 s) before the template copies files. Tested under Wine with a real running exe: - without the hook the copy fails (the bug); - with it, the installer waits out the remaining ~2.8 s, then copies; - a manual install (no `/UPDATE`) doesn't wait; - a stuck process hits the 15 s cap and falls through to the template's own running-app handling. `build-windows` compiled it (run 2057). **Leaving open until a real auto-update onto a build with this hook completes without the "Error opening file for writing" dialog.** The installer that runs is the *new* version's, so the first update onto this build is already the test.
Author
Owner

Re-check (2026-09-28)

The fix is in every build users update to. The update feed (releases/download/latest/release.json) serves 4.12.347 (published 2026-09-29 00:35 UTC). installerHooks: ./nsis/hooks.nsh has been in tauri.conf.json on main since 4648c8c, and every build since 23 Sep comes from there. The installer that runs during an update is the new version's, so every update since then has gone through the hook.

Review of the hook against the current upstream template and plugin:

  • Flag: tauri-plugin-updater 2.10.1 passes /UPDATE to the installer (updater.rs:812), so the hook's ${GetOptions} $CMDLINE "/UPDATE" gate fires on auto-updates only.
  • Order: in Tauri's installer.nsi, Section Install runs NSIS_HOOK_PREINSTALL (our wait) → CheckIfAppIsRunning (the template's kill) → File (the copy). So the wait happens before the copy, and a process stuck past 15 s still gets the template's kill.
  • Probe: FileOpen … a on an existing file opens it for writing without truncating it, and it's closed straight away, so the exe isn't touched.
  • Leftover edge case: if the old process hangs for more than 15 s, the template kills it and sleeps only 500 ms before copying, so the original race could still happen there. That's rare (a hung process); Retry still works in that case.

Still needed: one real Windows auto-update between two builds from after 23 Sep, with no "Error opening file for writing" dialog. If nobody has seen the dialog since, this can be closed.

## Re-check (2026-09-28) **The fix is in every build users update to.** The update feed (`releases/download/latest/release.json`) serves **4.12.347** (published 2026-09-29 00:35 UTC). `installerHooks: ./nsis/hooks.nsh` has been in `tauri.conf.json` on `main` since `4648c8c`, and every build since 23 Sep comes from there. The installer that runs during an update is the new version's, so every update since then has gone through the hook. **Review of the hook against the current upstream template and plugin:** - **Flag:** `tauri-plugin-updater` 2.10.1 passes `/UPDATE` to the installer (`updater.rs:812`), so the hook's `${GetOptions} $CMDLINE "/UPDATE"` gate fires on auto-updates only. - **Order:** in Tauri's `installer.nsi`, `Section Install` runs `NSIS_HOOK_PREINSTALL` (our wait) → `CheckIfAppIsRunning` (the template's kill) → `File` (the copy). So the wait happens before the copy, and a process stuck past 15 s still gets the template's kill. - **Probe:** `FileOpen … a` on an existing file opens it for writing without truncating it, and it's closed straight away, so the exe isn't touched. - **Leftover edge case:** if the old process hangs for more than 15 s, the template kills it and sleeps only 500 ms before copying, so the original race could still happen there. That's rare (a hung process); Retry still works in that case. **Still needed:** one real Windows auto-update between two builds from after 23 Sep, with **no** "Error opening file for writing" dialog. If nobody has seen the dialog since, this can be closed.
Author
Owner

Confirmed fixed on a real Windows machine (2026-10-21, windows runner, run 2797).

The new smoke step does what the auto-updater does. With the app running, it starts the installer in passive update mode (/P /UPDATE), and the app only goes away 3 s later.

Result: the installer waited, then finished by itself. Exit code 0, no "Error opening file for writing" dialog, and cinny.exe rewritten.

The step now runs with every Windows smoke run (PR #42), so a regression would show up there. Closing.

**Confirmed fixed on a real Windows machine** (2026-10-21, `windows` runner, run 2797). The new smoke step does what the auto-updater does. With the app running, it starts the installer in passive update mode (`/P /UPDATE`), and the app only goes away 3 s later. Result: the installer waited, then finished by itself. Exit code 0, **no "Error opening file for writing" dialog**, and `cinny.exe` rewritten. The step now runs with every Windows smoke run (PR #42), so a regression would show up there. Closing.
jared closed this issue 2026-10-21 00:01:31 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny-desktop#1