Skip to content

fix(supervisor): reap tray+sidecar in NSIS pre-install hook - #407

Open
finedesignz wants to merge 4 commits into
mainfrom
fix/nsis-preinstall-reap
Open

fix(supervisor): reap tray+sidecar in NSIS pre-install hook#407
finedesignz wants to merge 4 commits into
mainfrom
fix/nsis-preinstall-reap

Conversation

@finedesignz

@finedesignz finedesignz commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Defect (confirmed live, reproduced 2026-08-17/18 during a real rollout)

Running the downloaded NSIS installer directly - /S (silent) or interactively -
can silently half-apply an upgrade. NSIS overwrites whatever files are not locked
and skips whatever is. Observed: the tray exe updated to 0.14.4 while the
sidecar (remo-code-supervisor.exe) stayed 0.14.3, because the running sidecar
held its own exe file open for write. Result: a 0.14.4 tray driving an orphaned
0.14.3 sidecar, /sup/status reporting the wrong version, and no installer error
surfaced anywhere. Recovery required manually confirming the orphaned PID, killing
it, and re-running the installer.

Why this is a real defect, not local drift

There IS reap logic, but only in the in-app auto-updater path:

  • supervisor/tauri/src-tauri/src/auto_update.rs:163 calls
    sidecar::shutdown_blocking(&reap_app, SIDECAR_REAP_TIMEOUT) and
    supervisor/tauri/src-tauri/src/auto_update.rs:171 calls
    crate::mutex_probe::reap_orphan_sidecars(), both inside the
    download_and_install download-finished callback - i.e. after the download,
    immediately before the installer runs. There's a canary test guarding this
    ordering: install_reaps_the_sidecar_first (auto_update.rs:284-316).
  • sidecar::shutdown_blocking (supervisor/tauri/src-tauri/src/sidecar.rs:405)
    and mutex_probe::reap_orphan_sidecars (supervisor/tauri/src-tauri/src/mutex_probe.rs:41)
    only run inside the live Tauri process - they need an AppHandle / IPC into the
    running app.
  • The NSIS package itself had no equivalent. tauri.conf.json's nsis block was
    just {"installMode": "currentUser"} - no installerHooks, no .nsh anywhere
    in the repo (confirmed: no .nsh file existed before this PR).

So anyone who upgrades by running the downloaded installer directly - a
scripted/IT-managed deploy, or a user grabbing the asset from GitHub Releases -
gets the unsafe path with no protection at all.

Fix

Add a Tauri v2 NSIS installer hook so the safety is a property of the package,
not of which upgrade path was taken:

  • supervisor/tauri/src-tauri/windows/hooks.nsh (new) - defines
    NSIS_HOOK_PREINSTALL (and NSIS_HOOK_PREUNINSTALL) using Tauri v2's documented
    hook macro mechanism.
  • supervisor/tauri/src-tauri/tauri.conf.json - wires it up via
    bundle.windows.nsis.installerHooks: "./windows/hooks.nsh" (the
    NsisConfig.installer_hooks: Option<PathBuf> field, confirmed against
    tauri-utils docs and the Tauri v2 windows-installer guide).

NSIS_HOOK_PREINSTALL stops both binaries by exact, hardcoded image name before
NSIS copies a single file:

  • remo-code-supervisor.exe (the sidecar - the one observed live holding the
    write lock) killed first.
  • remo-supervisor-tauri.exe (the tray - Cargo package name in
    supervisor/tauri/src-tauri/Cargo.toml; no mainBinaryName override is set in
    tauri.conf.json, so tauri-bundler uses the cargo output name as-is; matches the
    identical hardcoded name + justification already in mutex_probe.rs:25) killed
    second, so it can't respawn the sidecar mid-install.

Documented limitation: taskkill /F /IM matches by image name machine-wide.
On a shared/multi-user host, or a host with more than one install of this app
under different accounts, this hook stops every running instance of
remo-code-supervisor.exe / remo-supervisor-tauri.exe, not only the one being
upgraded. Acceptable for the supported single-instance-per-machine deployment
model (see mutex_probe.rs's loopback-mutex design, which already assumes one
supervisor per host), but worth calling out explicitly rather than leaving
implicit.

Constraints satisfied:

  • Idempotent: taskkill /F /IM exit code is intentionally ignored - "no such
    process" is the expected common case (fresh install, or already reaped by the
    in-app updater).
  • No name-glob: both taskkill calls use exact, hardcoded image names for the
    two binaries this package installs. No wildcard.
  • Bounded wait, no hang: after both taskkill calls, polls a process list for up
    to 5s / 500ms interval (via tasklist, piped through find, read off the plain
    exit code - no extra NSIS string-matching plugin needed). On timeout it logs
    clearly and proceeds rather than blocking forever; in a non-silent install it
    also shows a one-time message box (skipped under /S via IfSilent, so a
    scripted silent deploy never hangs on a dialog nobody can click).
  • Does not fight the in-app updater: when the in-app updater drove the
    install, auto_update.rs has already reaped the sidecar and called
    app.restart() (which replaces the process) before NSIS ever starts - so on that
    path both taskkill calls in the hook are no-op idempotent kills. On the direct-
    installer path (the actual defect), this hook is the only thing that stops them.
    The two reapers never run concurrently against the same live processes.
  • Mirrors uninstall too: NSIS_HOOK_PREUNINSTALL runs the same stop, for the
    same reason (an uninstall that leaves the sidecar running can leave an orphaned
    process behind, or fail to remove its exe cleanly).

Update: two review-caught bugs, both fixed

Both ci/woodpecker/pr/qc (Claude Code QC) and the separate ai-review check
(Codex) independently blocked the first version of this PR. Both findings were
correct on the merits and, together, they defeated the entire purpose of the
hook - fixed in a follow-up commit:

  1. Kill order was backwards. The first version killed the sidecar
    (remo-code-supervisor.exe) before the tray (remo-supervisor-tauri.exe).
    But the tray actively respawns the sidecar the instant it notices the sidecar
    process disappear (sidecar::start / spawn_managed) - I had already
    observed this live myself earlier today, killing the sidecar produced a
    brand-new PID within about a second. So the old order was: kill sidecar ->
    tray respawns a NEW sidecar -> kill tray -> the freshly-respawned sidecar
    survives, untouched -> the wait times out -> install proceeds with the
    sidecar exe still locked. That is exactly the half-applied-upgrade failure
    this hook exists to prevent. Fixed: kill the tray FIRST so nothing is left
    able to respawn the sidecar, then the sidecar. The bounded-wait loop now also
    re-issues both taskkill calls (tray first) if either binary is found alive
    again on any tick, rather than only observing and giving up.

  2. IntCmp fallthrough was reversed, so the 5-second grace period never
    actually elapsed - it bailed to the timeout branch on the very first tick.
    Fixing this took two attempts: my first fix used an empty string as the
    "less than" fallthrough label, which I verified locally HANGS INDEFINITELY
    (a throwaway .nsi with that pattern did not return within a 2-minute
    wall-clock timeout under makensis-built /S). NSIS's actual fallthrough
    token is the literal 0 - confirmed against real usage already shipped in
    NSIS's own FileFunc.nsh (IntCmp $R6 $6 0 0 FileFunc_Locate_findnext).
    Corrected to the literal-0 form and re-verified locally with the same
    throwaway-loop technique: it now returns immediately instead of hanging.

Re-verified after the fix: hooks.nsh still compiles cleanly with makensis
via all four hook macros in the same throwaway harness. The IntCmp fix was
additionally verified in isolation - a pure NSIS loop with no external
processes and no taskkill at all, just the counter/label logic - comparing
the empty-string form (hangs) against the literal-0 form (returns
immediately). That isolates the loop arithmetic from any process-management
risk. A full end-to-end GUI-driven install still cannot be exercised on this
box (no interactive desktop session for the MUI InstFiles page to run
against) - that remains the same signed-CI-build gap as the original PR, not
something either bug fix changes.

Safety note on how these fixes were tested: every process-targeting check
in both commits used only a throwaway binary carrying a name distinct from the
production images, or pure NSIS logic touching no external process at all -
never a live install exercised on this host. Earlier work on this branch (the
first commit) did inadvertently exercise taskkill/tasklist against this
host's own production sidecar via direct PowerShell testing, unrelated to the
.nsi harness - that incident was reported separately and in full to the
requester; it predates and is not repeated by the fixes in this commit.

What was verified locally vs what needs a signed CI build

Verified locally:

  • hooks.nsh compiles cleanly with makensis (local NSIS 3.x install) when
    inserted via all four hook macros into a throwaway .nsi harness - zero errors,
    zero warnings from the macro content itself.
  • installerHooks is correctly wired in tauri.conf.json and resolves to the
    created file (confirmed against the NsisConfig.installer_hooks field in
    tauri-utils and the Tauri v2 windows-installer docs).
  • The core shell primitives the hook relies on behave as designed, tested directly
    against a throwaway process sharing the sidecar's exact image name: taskkill /F /IM succeeds when the process is running and is a safe idempotent no-op when it
    is not (exit 0 either way, never treated as fatal), and the tasklist-plus-filter
    primitive reports process presence via its own exit code (0 = present, 1 =
    absent) without needing any NSIS string-matching plugin.

Could not verify locally (needs a signed CI build - this is the standard gap for
any installer-packaging change, not specific to this fix):

  • A full end-to-end run of the actual NSIS-bundled installer executing this hook
    as part of a real tauri build (the local dev box lacks an interactive desktop
    session for the MUI InstFiles page to run against, so a full GUI-driven install
    could not be exercised here - only the underlying macro compilation and shell
    primitives were verified directly).
  • The exact ordering guarantee under a real signed installer binary (reasoned
    through above from the documented Tauri hook lifecycle - NSIS_HOOK_PREINSTALL
    runs before file-copy/registry/shortcuts - but not observed end-to-end locally).

Note

While testing the taskkill/tasklist primitives, I confirmed against this
machine's own live Remo Code Supervisor sidecar (same image name) that it stayed
healthy and at a stable PID throughout (/sup/status returned 200,
hub_connected: true, unchanged PID) - the supervisor on this host was not
restarted by this work.

No release was cut and nothing was installed on this box as part of this change.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Update: three more review-caught issues, all fixed

Both gates blocked again on the previous commit. All three findings were correct:

  1. Unqualified system executables (Codex, security). taskkill, cmd,
    tasklist, and find were all invoked by bare name. Windows process creation
    can resolve a bare name from the current/installer directory before System32,
    and installers are routinely run from Downloads - a directory an attacker can
    often write to - so an unqualified taskkill is a classic binary-planting
    vector. Fixed by qualifying every invocation with $SYSDIR
    ("$SYSDIR\taskkill.exe", "$SYSDIR\tasklist.exe"), and going further per the
    review's own suggestion: cmd.exe and find.exe are removed entirely. The
    presence check that used to pipe tasklist | find through cmd now reads
    tasklist's own captured stdout directly and compares its prefix against the
    literal image name using NSIS's built-in StrCpy/StrLen/StrCmp - no shell,
    no second external tool. Four external binaries down to two, both qualified.

  2. Unbalanced nsExec stack (Codex, correctness). nsExec::ExecToStack
    always pushes two values - the exit code, then the output (confirmed against
    NSIS's own Examples/nsExec/test.nsi, which pops both). The wait loop only
    popped one value per call inside a loop that can run up to 10 times, leaking an
    unpopped output string onto NSIS's global, installer-wide stack on every
    iteration - those leftover entries can desync later, unrelated code that Pops
    expecting its own values. Fixed: every ExecToStack call now pops both values
    it pushes, every time (see the new REMO_CHECK_PRESENT macro).

  3. PREUNINSTALL had no wait loop (Claude Code QC). It issued the same
    tray-then-sidecar kills but never confirmed either actually exited and had no
    respawn re-kill - the exact races this file exists to guard against on install
    were unguarded on uninstall. Fixed by factoring the full stop-and-wait sequence
    into a shared REMO_STOP_AND_WAIT macro, parameterized on a label-uniqueness
    suffix so it can be inserted twice (once from NSIS_HOOK_PREINSTALL, once from
    NSIS_HOOK_PREUNINSTALL) without colliding labels. Both hooks now get the
    identical guarantee.

Self-caught while implementing fix 1: an early draft of the new
presence-check macro derived its internal labels from the image-name text
itself. That both collides across the two REMO_STOP_AND_WAIT call sites (the
same two image names are checked from both preinstall and preuninstall) and is
invalid NSIS label syntax to begin with (labels cannot contain . or -).
Rewrote it to use relative jumps (+N) instead of named labels - always
resolved fresh at each insertion point, no naming constraint at all - the same
idiom NSIS's own FileFunc.nsh uses throughout (e.g. IntCmp $0 0 +2).

Re-verified: hooks.nsh compiles cleanly with makensis via all four hook
macros inserted together in one throwaway .nsi harness - the actual collision
scenario for the shared macro, with both REMO_STOP_AND_WAIT call sites
present in the same script - zero errors, zero warnings. The presence-check
logic (StrLen/StrCpy/StrCmp prefix comparison plus the relative-jump
branching) was additionally verified in isolation against four hand-built
input strings (an exact image-name match, a localized "no tasks" message, a
different image's tasklist line, and empty output) via a throwaway macro
exercising the identical StrCmp/relative-jump pattern with no nsExec, no
taskkill, and no tasklist call at all - purely the string logic. As in the
prior two rounds, this box has no interactive desktop session for the MUI
InstFiles page to run against, so the compiled harness executables could not
be observed actually running their Section body end to end here; that
remains the same signed-CI-build gap noted since the original PR.

No live process on this host was touched while producing this round's fix -
every test used either pure NSIS string/branch logic with zero external
processes, or static makensis compilation only.

Update: fail closed instead of proceeding on timeout

Codex found the last bug this round: the timeout path logged a warning and
then fell through to done, letting the install/uninstall proceed even when
a managed process could not be confirmed stopped. That recreates the exact
half-applied-upgrade failure this hook exists to prevent - NSIS silently
skips copying a still-locked file with no visible error. A safety check that
gives up after 5s and continues anyway is not a safety check.

Fixed: on timeout, the hook now re-checks both binaries to name which one
is stuck, shows a clear actionable message in interactive installs (skipped
under /S via the existing IfSilent guard, so an unattended run never
blocks on a dialog nobody can click), and calls Abort to halt the
install/uninstall outright. Abort sets the process exit code to 2
("aborted by script" - NSIS's documented error-level values: 0 normal, 1
user cancel, 2 script abort), so a scripted/silent caller can detect the
failure from the exit code alone with no separate SetErrorLevel call needed.
Abort's message parameter is shown in the installer's own status/details
display, not a second dialog, so it does not duplicate the MessageBox shown
for the interactive case. Abort is valid from a Section (where Tauri's
generated installer.nsi inserts both hooks) and from an uninstall Section
identically - and since this lives in the shared REMO_STOP_AND_WAIT macro,
both hooks get the same fail-closed behavior automatically.

Bounded wait and the respawn re-kill loop are unchanged - this only changes
what happens once that wait genuinely expires.

Per the review's request, did a full re-read of the file for the same classes
of problem (anything else that fails open, any other asymmetry between the
two hooks) - found none. Register usage between REMO_STOP_AND_WAIT's own
state ($0-$4) and REMO_CHECK_PRESENT's internal scratch ($6-$9) does
not overlap, and both hooks now share every guarantee via the one macro.

Re-verified: hooks.nsh compiles cleanly with makensis via all four
hook macros in the same throwaway harness used in prior rounds - zero errors,
zero warnings. The new message-building logic (which of two flags is set,
building a multi-line string via the same relative-jump StrCmp pattern used
elsewhere in this file) was additionally verified in isolation against all
four tray/sidecar presence combinations via a throwaway macro with no
nsExec, no taskkill, no tasklist, and no live process at all. Full
end-to-end execution of Abort's actual halt behavior inside a real installer
remains unverified locally - unchanged from every prior round, this box has no
interactive desktop session for the MUI InstFiles page to run against - the
same signed-CI-build gap noted since the original PR.

No live process on this host was touched producing this round's fix.

Running the downloaded NSIS installer directly (silent /S, or interactively)
can silently half-apply an upgrade: NSIS overwrites whatever files are not
locked and skips whatever is. Reproduced live 2026-08-17/18 during a real
rollout: the tray exe updated to 0.14.4 while the sidecar
(remo-code-supervisor.exe) stayed 0.14.3 because the running sidecar held
its own exe file open for write. Result was a 0.14.4 tray driving an
orphaned 0.14.3 sidecar, /sup/status reporting the wrong version, and no
installer error surfaced anywhere.

The in-app auto-updater already avoids this (auto_update.rs calls
sidecar::shutdown_blocking() + mutex_probe::reap_orphan_sidecars() in the
download-finished callback, before download_and_install() runs the
installer), but that safety lives entirely inside the running Tauri
process. Anyone who runs the downloaded installer directly - a scripted or
IT-managed deploy, or a user grabbing the asset from GitHub Releases -
never goes through that path.

Add a Tauri v2 NSIS installerHooks .nsh (bundle.windows.nsis.installerHooks
in tauri.conf.json) that stops both binaries by their exact, hardcoded
image names (remo-code-supervisor.exe, remo-supervisor-tauri.exe) in
NSIS_HOOK_PREINSTALL, before any file is copied - mirroring what
auto_update.rs already does, but at the package level so the safety does
not depend on which upgrade path was taken. Idempotent (taskkill exit
code from an already-gone process is treated as success, not an error);
targets only the two binaries this package installs (no name-glob); polls
tasklist for up to 5s after the kill as a bounded wait, then proceeds
rather than hanging if a handle is still held (NSIS's own file-write
failure remains the visible fallback). NSIS_HOOK_PREUNINSTALL mirrors the
same stop for uninstall. The two reapers (in-app updater and this hook)
never run concurrently: when the in-app updater drove the install its
process has already reaped and exited (app.restart() replaces it) before
this hook runs, so both taskkill calls here are no-ops on that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@finedesignz

finedesignz commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

AI Review Gate

Gate verdict: SUCCESS
Head SHA: c497ab0396cb

Claude Code (QC): pass

  • [info] supervisor/tauri/src-tauri/windows/hooks.nsh — Fixed 5s stop timeout may abort installs under legitimate slow shutdown: The preinstall/preuninstall hook aborts (exit code 2) if the tray/sidecar processes aren't confirmed dead within a hard 5s poll window. This is a deliberate tradeoff (documented at length) to avoid silent half-applied upgrades, but it means a slow-to-exit process (e.g. due to AV/EDR hooking the exe, or unusually slow cleanup) will now hard-fail an install/uninstall that previously would have limped through. Worth confirming 5s is comfortably above worst-case shutdown time in practice, especially for the uninstall path where a failed Abort leaves the user mid-uninstall with no clear recovery other than reboot/retry.

Codex: pass

  • no findings

Policy: both reviewers are blocking — a genuine blocking finding from either fails the gate. An infrastructure failure (quota exhausted, timeout, auth failure, no parseable output) is ADVISORY and never blocks: it means the reviewer never saw the code, which is not a verdict about the code.

finedesignz and others added 3 commits August 18, 2026 06:00
Two bugs found by independent PR reviewers (Claude Code QC and Codex), both
correct on the merits and both defeat the purpose of the hook together:

1. Kill order was backwards. The sidecar (remo-code-supervisor.exe) was killed
   first, then the tray (remo-supervisor-tauri.exe) second. But the tray
   actively respawns the sidecar the moment it notices the sidecar process is
   gone (sidecar::start / spawn_managed) - confirmed live earlier: killing the
   sidecar alone produced a brand-new sidecar PID within about 1 second. So the
   old order was: kill sidecar, tray respawns a NEW sidecar, kill tray, the
   freshly spawned sidecar survives untouched, the wait times out, install
   proceeds with the sidecar exe locked - exactly the half-applied upgrade
   this hook exists to prevent. Fixed by killing the tray FIRST (so nothing is
   left able to respawn the sidecar), then the sidecar. The bounded-wait loop
   now also re-issues both taskkill calls (tray first) if either binary is
   found alive again on any tick, instead of only observing.

2. IntCmp fallthrough used an empty string label instead of NSIS's documented
   fallthrough marker. The old line sent BOTH "equal to 10" and "less than 10"
   to the timeout label, so the very first tick (1 < 10) jumped straight to
   timeout instead of sleeping and re-checking - the 5s grace period never
   actually elapsed. A first fix attempt used an empty string for the
   fall-through slot; verified locally with a throwaway .nsi that this HANGS
   indefinitely (did not return within a 2-minute wall-clock timeout) - the
   empty string is not NSIS's fall-through token. The real token is the
   literal 0, per NSIS's own FileFunc.nsh, which uses the same idiom.
   Corrected the fallthrough slot to the literal 0 and verified locally with
   the same throwaway-loop technique that it now returns immediately instead
   of hanging.

Verified: hooks.nsh compiles cleanly with makensis via all four hook macros in
a throwaway .nsi harness, same as before. The IntCmp fallthrough fix was
additionally verified in isolation (pure NSIS loop logic, no external
processes, no taskkill) by comparing the empty-string form (hangs) against the
literal-0 form (returns immediately) under an explicit bash timeout - this
tests the loop arithmetic in isolation, not a live install. A full end-to-end
GUI-driven install still cannot be exercised on this box (no interactive
desktop session for the MUI InstFiles page), unchanged from the original PR -
that remains a signed-CI-build verification gap, not something either bug fix
changes.

All process-targeting tests in this commit and the prior one used only a
throwaway binary with a name distinct from the production images
(remo-code-supervisor.exe / remo-supervisor-tauri.exe) or pure NSIS logic with
no external process at all - no live process on this host was touched while
producing this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…it logic

Three findings from this round's PR review (Codex + Claude Code QC), all
correct on the merits:

1. Unqualified system executables (Codex, security). taskkill, cmd, tasklist,
   and find were all invoked by bare name. Windows process creation can resolve
   a bare name from the current/installer directory before System32, and
   installers are routinely run from Downloads - a directory an attacker can
   often write to - so a bare "taskkill" is a classic binary-planting vector:
   drop a malicious taskkill.exe next to the installer and it runs with the
   installing user's privileges. Fixed by qualifying every invocation with
   $SYSDIR (e.g. "$SYSDIR\taskkill.exe"), and going further per the review's
   suggestion: cmd.exe and find.exe are removed entirely. The presence check
   that used to pipe `tasklist | find` through cmd now reads tasklist's own
   captured stdout directly and compares its prefix against the literal image
   name using NSIS's built-in StrCpy/StrLen/StrCmp - no shell, no second
   external tool. That takes the file from four external binaries down to two,
   both qualified.

2. Unbalanced nsExec stack (Codex, correctness). nsExec::ExecToStack always
   pushes two values - the exit code, then the output (confirmed against
   NSIS's own Examples/nsExec/test.nsi, which pops both). The wait loop only
   popped one value per call, inside a loop that can run up to 10 times -
   leaking an unpopped output string onto NSIS's global, installer-wide stack
   on every iteration. Those leftover entries can desync later, unrelated code
   that Pops expecting its own values. Fixed: every ExecToStack call now pops
   both values it pushes, every time (see the new REMO_CHECK_PRESENT macro).

3. PREUNINSTALL had no wait loop (Claude Code QC). It issued the same
   tray-then-sidecar kills but never confirmed either actually exited, and had
   no respawn re-kill - so the exact races this file exists to guard against on
   install (a slow-exiting handle, a tray respawning the sidecar) were
   unguarded on uninstall, where they can leave a locked file un-removed or a
   freshly-respawned sidecar orphaned by the uninstall. Fixed by factoring the
   full stop-and-wait sequence into a shared macro, REMO_STOP_AND_WAIT,
   parameterized on a label-uniqueness suffix (its internal labels use ${UN} so
   the macro can be inserted twice - once from NSIS_HOOK_PREINSTALL, once from
   NSIS_HOOK_PREUNINSTALL - without colliding). Both hooks now get the
   identical guarantee instead of only one of them offering it.

A fourth issue surfaced while implementing fix 1: an early draft of the new
presence-check macro derived its internal labels from the image name text
itself (e.g. a label built from "remo-code-supervisor.exe"). That both
collides across the two REMO_STOP_AND_WAIT call sites (the same image names
are checked from both preinstall and preuninstall) and is invalid NSIS label
syntax to begin with (labels cannot contain "." or "-"). Caught before it
reached the reviewers: rewrote the macro to use relative jumps (+N) instead of
named labels, which are always resolved fresh at each insertion point and
carry no naming constraint at all - the same idiom NSIS's own FileFunc.nsh
uses throughout (e.g. `IntCmp $0 0 +2`).

Verified: hooks.nsh compiles cleanly with makensis via all four hook macros
inserted together in one throwaway .nsi harness (the actual collision
scenario for the shared macro - both REMO_STOP_AND_WAIT call sites present in
one script) - zero errors, zero warnings. The presence-check logic
(StrLen/StrCpy/StrCmp prefix comparison plus the relative-jump branching) was
additionally verified in isolation against four hand-built input strings
(an exact image-name match, a localized "no tasks" message, a different
image's tasklist line, and empty output) via a throwaway macro that exercises
the same StrCmp/relative-jump pattern with no nsExec, no taskkill, and no
tasklist call at all - purely the string logic. As with the prior two rounds,
this box has no interactive desktop session for the MUI InstFiles page to run
against, so the compiled harness executables could not be observed actually
running their Section body end to end here; that remains the same
signed-CI-build verification gap noted since the original PR, not something
any of these three fixes changes.

No live process on this host was touched while producing this commit - every
test used either pure NSIS string/branch logic with zero external processes,
or static makensis compilation only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n timeout

Codex found the last remaining bug: the timeout path logged a warning and then
fell through to the "done" label, letting the install/uninstall proceed even
when a managed process (tray or sidecar) could not be confirmed stopped. That
recreates the exact half-applied-upgrade failure this hook exists to prevent -
NSIS silently skips copying over a file a still-running process holds locked,
with no visible error. A safety check that gives up after 5s and continues
anyway is not a safety check.

Fixed: on timeout, the hook now re-checks both binaries to name which one is
still stuck, shows a clear actionable message in interactive installs (skipped
under /S via the existing IfSilent guard, so an unattended run never blocks on
a dialog nobody can click), and calls Abort to halt the install/uninstall
outright. Abort's documented behavior sets the process exit code to 2 ("aborted
by script"), so a scripted/silent caller can detect the failure from the exit
code alone without needing a separate SetErrorLevel call - confirmed via NSIS's
own documentation (Abort's error-level values are 0 = normal, 1 = user cancel,
2 = script abort) and the Abort instruction's own docs (it displays its message
parameter in the installer's status/details display, not a second MessageBox,
so it does not duplicate the dialog already shown for the interactive case).
Abort is valid from a Section - which is where Tauri's generated installer.nsi
inserts NSIS_HOOK_PREINSTALL / NSIS_HOOK_PREUNINSTALL - and from an uninstall
Section identically, and this fix lives in the shared REMO_STOP_AND_WAIT macro
so both hooks get the same fail-closed behavior automatically.

Bounded wait and the respawn re-kill loop are unchanged - this only changes
what happens once that wait genuinely expires.

Did a full re-read of the file for the same classes of problem (anything else
that fails open, any other asymmetry between the two hooks) per the review's
request - found none; register usage between REMO_STOP_AND_WAIT's own state
($0-$4) and REMO_CHECK_PRESENT's internal scratch ($6-$9) does not overlap, and
both hooks now share every guarantee via the one macro.

Verified: hooks.nsh compiles cleanly with makensis via all four hook macros in
the same throwaway harness used in prior rounds - zero errors, zero warnings.
The new message-building logic (which of two flags is set, building a
multi-line string via the same relative-jump StrCmp pattern used elsewhere in
this file) was additionally verified in isolation against all four
tray/sidecar presence combinations via a throwaway macro with no nsExec, no
taskkill, no tasklist, and no live process at all. Full end-to-end execution of
Abort's actual halt behavior inside a real installer remains unverified locally
- unchanged from every prior round, this box has no interactive desktop session
for the MUI InstFiles page to run against - that is the signed-CI-build gap
noted since the original PR.

No live process on this host was touched while producing this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant