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:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -58,7 +59,17 @@ func acquireInstance(wait bool) bool {
|
||||
if !wait {
|
||||
return false
|
||||
}
|
||||
deadline := time.Now().Add(20 * time.Second)
|
||||
// Forty-five seconds, not twenty.
|
||||
//
|
||||
// The instance we are waiting for is allowed THIRTY to shut down — see
|
||||
// armExitWatchdog, which force-exits it at that point — because it closes a
|
||||
// remote logbook, a CAT session and sometimes a backup on the way out. A
|
||||
// twenty-second patience was therefore shorter than the wait it existed for,
|
||||
// and on a station where shutdown ran long the new instance gave up while
|
||||
// the old one was still finishing: no new window after an update, and a
|
||||
// leftover OpsLog in the task manager. This is the backstop; --wait-pid
|
||||
// below is the real answer.
|
||||
deadline := time.Now().Add(45 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
if acquireSingleInstance() {
|
||||
@@ -68,6 +79,23 @@ func acquireInstance(wait bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// waitPidArg reads "--wait-pid N": the process this one must outlive.
|
||||
func waitPidArg(args []string) int {
|
||||
for i, a := range args {
|
||||
if a == "--wait-pid" && i+1 < len(args) {
|
||||
if n, err := strconv.Atoi(args[i+1]); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
if v, ok := strings.CutPrefix(a, "--wait-pid="); ok {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// processStart is stamped on the very first instruction of main, before the
|
||||
// single-instance guard and before anything else runs.
|
||||
//
|
||||
@@ -90,6 +118,20 @@ func main() {
|
||||
// to free instead of bailing out. Then clear the old exe it left behind.
|
||||
bootLogLaunch()
|
||||
postUpdate := hasFlag(os.Args[1:], "--post-update")
|
||||
// The instance that started us is still shutting down. Wait for it to
|
||||
// actually END — a kernel wait on its handle, which finishes the instant it
|
||||
// does — rather than hoping the mutex frees inside a fixed window. This is
|
||||
// what the old PowerShell helper did, and losing it is what left an operator
|
||||
// with no window after an update and the previous OpsLog still in the task
|
||||
// manager.
|
||||
if pid := waitPidArg(os.Args[1:]); pid > 0 {
|
||||
bootLog("waiting for the previous instance (pid %d) to exit", pid)
|
||||
if waitForProcessExit(pid, 60*time.Second) {
|
||||
bootLog("the previous instance is gone")
|
||||
} else {
|
||||
bootLog("the previous instance (pid %d) is STILL running after 60s — trying anyway", pid)
|
||||
}
|
||||
}
|
||||
// A self-relaunch (database switch) races its own parent: the new process
|
||||
// regularly wins the start against the old one's teardown, and the operator
|
||||
// got "OpsLog is already running" for following instructions. Same patience
|
||||
@@ -100,7 +142,16 @@ func main() {
|
||||
// window, no data folder, no log, which is indistinguishable from a
|
||||
// program that died on its first instruction.
|
||||
bootLog("another instance already holds the single-instance mutex - exiting")
|
||||
fatalBox("OpsLog", "OpsLog is already running.\n\nLook for its window, or for a leftover OpsLog.exe in the Task Manager, and close it before starting another.")
|
||||
if postUpdate {
|
||||
// After an update the ordinary message is a lie by omission: the
|
||||
// operator did not start a second copy, the update did, and what
|
||||
// they need to know is that the PREVIOUS version never finished
|
||||
// closing.
|
||||
fatalBox("OpsLog", "The previous version of OpsLog has not finished closing, so the updated one cannot start.\n\n"+
|
||||
"Close the leftover OpsLog.exe in the Task Manager, then start OpsLog again — the update is already installed.")
|
||||
} else {
|
||||
fatalBox("OpsLog", "OpsLog is already running.\n\nLook for its window, or for a leftover OpsLog.exe in the Task Manager, and close it before starting another.")
|
||||
}
|
||||
return
|
||||
}
|
||||
bootLog("single-instance mutex acquired")
|
||||
|
||||
Reference in New Issue
Block a user