fix(update): wait for the old process, not for a fixed window

The relaunch after an update stopped working, and the regression is mine:
removing the PowerShell helper — which is what Defender was reading as a
dropper — also removed the wait it was doing. Nothing took over the job.

The numbers made it certain rather than unlucky. The instance being
replaced is allowed THIRTY seconds to shut down (armExitWatchdog forces
it out at that point) because it closes a remote logbook, a CAT session
and sometimes a backup. The new instance was patient with the
single-instance mutex for TWENTY. On any station where shutting down ran
past that, the new process gave up and exited in silence: no window after
an update, and the previous OpsLog still in the task manager. Exactly the
report.

Both relaunch paths now pass --wait-pid, and the new process waits on
that process's handle — a plain kernel wait, which ends the instant the
old one ends, however long or short that is, and looks nothing like a
script starting another program. The mutex retry stays as a backstop and
goes to forty-five seconds, so it is longer than the wait it exists for
rather than shorter.

And when the old process really has not gone, the message says that
instead of "OpsLog is already running" — after an update the operator did
not start a second copy, and what they need to know is which one to
close.

A test keeps the two spawn sites honest: a relaunch added without
--wait-pid is this bug again.
This commit is contained in:
2026-09-08 09:18:06 +02:00
parent 38da904f4b
commit e0b110392a
7 changed files with 162 additions and 10 deletions
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"os"
"regexp"
"strings"
"testing"
)
func TestWaitPidArg(t *testing.T) {
for _, tc := range []struct {
name string
args []string
want int
}{
{"after the flag", []string{"--post-update", "--wait-pid", "4321"}, 4321},
{"joined with =", []string{"--wait-pid=4321"}, 4321},
{"absent", []string{"--post-update"}, 0},
{"flag with nothing after it", []string{"--wait-pid"}, 0},
{"not a number", []string{"--wait-pid", "later"}, 0},
} {
if got := waitPidArg(tc.args); got != tc.want {
t.Errorf("%s: got %d, wanted %d", tc.name, got, tc.want)
}
}
}
// Every relaunch has to tell the new process which one to wait for.
//
// The auto-update relaunch lost that when its PowerShell helper was removed —
// the helper had waited for the pid, and nothing took over the job — and an
// operator was left with no window after an update and the previous OpsLog
// still running. This keeps the two spawn sites honest: if a relaunch is added
// without --wait-pid, it is the same bug again.
func TestEveryRelaunchPassesItsPid(t *testing.T) {
spawn := regexp.MustCompile(`exec\.Command\(exe, "--(post-update|relaunch)"[^)]*\)`)
for _, file := range []string{"update.go", "app.go"} {
src, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
for _, call := range spawn.FindAllString(string(src), -1) {
if !strings.Contains(call, "--wait-pid") {
t.Errorf("%s: %s does not tell the new instance which process to wait for", file, call)
}
}
}
}