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.
59 lines
2.4 KiB
Go
59 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A relaunch of OpsLog itself must not suppress the new process's window.
|
|
//
|
|
// SysProcAttr is where that damage was done: hideConsole sets HideWindow, which
|
|
// on Windows becomes SW_HIDE in the STARTUPINFO, and Windows applies it to the
|
|
// first top-level window the new process shows. The updated OpsLog started,
|
|
// took the single-instance mutex and connected the rig — invisibly. Two
|
|
// operators on 0.27.23 reported it as "it goes to reload and just fails to
|
|
// load": a process in the task manager, no window, and killing it then starting
|
|
// it by hand working every time.
|
|
//
|
|
// The window flags themselves are asserted in relaunch_windows_test.go, where
|
|
// SysProcAttr has the fields to look at.
|
|
func TestRelaunchCmdPassesItsArgumentsAndFolder(t *testing.T) {
|
|
cmd := relaunchCmd(filepath.Join("C:", "OpsLog", "OpsLog.exe"), "--post-update", "--wait-pid", "1234")
|
|
if len(cmd.Args) != 4 || cmd.Args[1] != "--post-update" || cmd.Args[3] != "1234" {
|
|
t.Errorf("args = %v, want the exe plus the three passed through", cmd.Args)
|
|
}
|
|
// The working directory matters: the new instance keeps its data folder
|
|
// beside the executable, and inheriting the old process's cwd would look for
|
|
// it somewhere else entirely.
|
|
if cmd.Dir != filepath.Join("C:", "OpsLog") {
|
|
t.Errorf("Dir = %q, want the executable's folder", cmd.Dir)
|
|
}
|
|
}
|
|
|
|
// The update relaunch goes through a helper that OUTLIVES this process.
|
|
//
|
|
// Two rewrites started the new exe from here instead, and both left operators
|
|
// with no window after an update: the launch then happens while this process is
|
|
// still alive and still holds the mutex. The helper waits for our pid first.
|
|
// Restored from before 0430aab and pinned here so a third rewrite has to argue
|
|
// with the two reports rather than rediscover them.
|
|
func TestUpdateRelaunchWaitsForUsFromOutside(t *testing.T) {
|
|
src, err := os.ReadFile("update.go")
|
|
if err != nil {
|
|
t.Fatalf("read update.go: %v", err)
|
|
}
|
|
got := string(src)
|
|
for _, want := range []string{"Wait-Process -Id", "Start-Process -FilePath"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("update.go no longer contains %q — the relaunch must wait for this process from outside it", want)
|
|
}
|
|
}
|
|
// Start-Process shows the new window normally. A direct exec.Command(exe…)
|
|
// here is the shape that broke it, twice.
|
|
if strings.Contains(got, "exec.Command(exe") {
|
|
t.Error("update.go starts the new exe directly again")
|
|
}
|
|
}
|