Restored verbatim from before 0430aab:
Wait-Process -Id <pid>; Start-Sleep -Milliseconds 400;
Start-Process -FilePath <exe> -ArgumentList '--post-update'
Two rewrites tried to start the new exe from inside the dying process
and both failed on real stations — 0.27.23 and again after the SW_HIDE
fix. The original's own comment had already said why: "Launching the new
exe directly while we're still alive raced the mutex and often left
nothing running." Telling the new instance our pid so it could wait on
the other side looked equivalent and is not. What the helper has that
neither rewrite did is that it OUTLIVES us — the launch happens once
this process is completely gone, from a process that was never our
child.
The Defender cost is known and accepted: an unsigned binary that
replaces itself, clears the mark-of-the-web and spawns a windowless
script to start another executable has the shape of a dropper, and
0.27.14 was removed from a station as Trojan:Script/Wacatac.H!ml. The
operator's decision is that whitelisting OpsLog beats an updater that
leaves people with no running program, and the changelog says so.
HideWindow stays on the PowerShell and is not the bug that made the
updated OpsLog invisible: it hides the helper's console, which is the
point, while Start-Process shows the new window normally.
relaunchCmd and its detachment stay for RestartApp, the database-switch
relaunch, which has never been reported broken.
Two tests kept honest: the update path must contain Wait-Process and
Start-Process and must not spawn the exe directly again, and
TestEveryRelaunchPassesItsPid had quietly stopped matching anything when
the spawn sites changed spelling — its pattern now covers relaunchCmd
too, so it guards RestartApp instead of passing vacuously.
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.