fix(update): put back the two things the PowerShell helper did

Operators are still losing the relaunch, so I went and read the code
from before 0430aab — the commit that removed the helper — instead of
theorising again. It was:

  Wait-Process -Id <pid>; Start-Sleep -Milliseconds 400;
  Start-Process -FilePath <exe> -ArgumentList '--post-update'

Two things in there that the direct launch never had:

1. Start-Process made the new OpsLog a child of PowerShell, which then
   exited — so it was detached. The direct launch makes it a child of
   the instance that is dying, in the same process group and console.
   DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP puts it back on its own,
   so nothing aimed at the old process can reach the new one.

2. It slept 400 ms AFTER the old process was gone, before starting
   anything. A process's handles are released by the kernel as it dies,
   so the mutex is free the moment the wait returns — but the things
   around it are not on that clock: the WebView2 user-data lock, the log
   file, an antivirus that woke up when the exe was replaced. This is
   not a theory about which of those it was; it is the pause being put
   back where it was.

Not restored: the PowerShell itself. Defender removed 0.27.14 from a
station as Trojan:Script/Wacatac.H!ml, and "Script/" was that helper —
an unsigned binary replacing itself and spawning a windowless script to
start another executable is, byte for byte, a dropper. Bringing it back
trades this fault for one that deletes the program.

Also: the relaunch now logs the child's pid, which the new instance's
startup.log already records on the other side. Without the pair there is
no telling "the new instance never started" from "it started and gave up
waiting", and those have different causes.
This commit is contained in:
2026-09-11 08:02:12 +02:00
parent c187f60415
commit 8d01097ca8
7 changed files with 126 additions and 9 deletions
+50
View File
@@ -0,0 +1,50 @@
//go:build windows
package main
import (
"path/filepath"
"testing"
)
// A relaunch of OpsLog itself must not suppress the new process's window.
//
// HideWindow becomes SW_HIDE in the STARTUPINFO, and Windows applies it to the
// first top-level window the new process shows. That is how the updated OpsLog
// came to start perfectly — mutex taken, rig connected — and stay invisible:
// two operators on 0.27.23 reported a process in the task manager, no window,
// and killing it then starting OpsLog by hand working every time.
//
// CREATE_NO_WINDOW is refused for the same reason it is pointless: there is no
// console to suppress on a GUI-subsystem binary, and it is one flag away from
// the one that broke this.
func TestRelaunchNeverHidesTheWindow(t *testing.T) {
cmd := relaunchCmd(filepath.Join("C:", "OpsLog", "OpsLog.exe"), "--post-update")
if cmd.SysProcAttr == nil {
t.Fatal("no SysProcAttr — the relaunch should be detached (see detachProcess)")
}
if cmd.SysProcAttr.HideWindow {
t.Error("HideWindow is set: the relaunched OpsLog would start with no window")
}
const createNoWindow = 0x08000000
if cmd.SysProcAttr.CreationFlags&createNoWindow != 0 {
t.Error("CREATE_NO_WINDOW is set on a GUI-subsystem relaunch")
}
}
// Detached and in its own process group, which is what Start-Process gave the
// relaunch before the PowerShell helper was removed. Being a child of the
// instance that is dying is the one difference from the code that worked.
func TestRelaunchIsDetached(t *testing.T) {
cmd := relaunchCmd(filepath.Join("C:", "OpsLog", "OpsLog.exe"), "--post-update")
const (
detachedProcess = 0x00000008
createNewProcessGroup = 0x00000200
)
if cmd.SysProcAttr.CreationFlags&detachedProcess == 0 {
t.Error("DETACHED_PROCESS is missing: the new instance keeps the old one's console")
}
if cmd.SysProcAttr.CreationFlags&createNewProcessGroup == 0 {
t.Error("CREATE_NEW_PROCESS_GROUP is missing: a cleanup aimed at the old instance can reach the new one")
}
}