//go:build windows package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" "hamlog/internal/applog" ) // clearDownloadMark strips the NTFS "downloaded from the internet" stream. // Otherwise Windows SmartScreen wants to prompt "are you sure you want to open // this?" — but since we launch the exe programmatically that prompt never // shows, and the launch is silently blocked. This is exactly why the relaunch // used to fail after an update. func clearDownloadMark(path string) { _ = os.Remove(path + ":Zone.Identifier") } // makeExecutable is a no-op on Windows, where the extension decides. func makeExecutable(path string) error { return nil } // scheduleDeferredSwap hands the exe swap to a detached helper that runs AFTER // this process is gone. // // The fallback for when the running image cannot be renamed at all. Once OpsLog // has exited its exe is an ordinary file again, so the move that was refused a // moment earlier succeeds — and the helper keeps trying for ten seconds, because // an antivirus that was holding the file usually lets go a beat after the // process dies rather than instantly. // // OpsLog is restarted either way. If the move failed, that starts the OLD build // — the update simply has not applied — and the operator keeps a working logger // instead of having it vanish mid-session, which for someone in a QSO is worse // than an update that waits. Only a successful swap passes --post-update, so a // failure leaves the .new file in place for the next attempt rather than having // the cleanup delete the download. // The LAST resort still needs a helper that outlives this process: nothing else // can move a file over an image that is still running. It stays PowerShell — // there is no smaller tool on a stock Windows that can wait for a pid and then // move a file — but it is reached only when the rename above failed, which is // rare, and never on the ordinary update path (see the relaunch there for why // that matters to Defender). func (a *App) scheduleDeferredSwap(exe, pending string) error { // Clear the "downloaded from the internet" mark before it becomes the exe — // SmartScreen silently blocks a programmatic launch of a marked file, and the // mark follows the file across the move. _ = os.Remove(pending + ":Zone.Identifier") q := func(s string) string { return strings.ReplaceAll(s, "'", "''") } ps := fmt.Sprintf( "Wait-Process -Id %d -ErrorAction SilentlyContinue; "+ "$ok=$false; "+ "for ($i=0; $i -lt 40; $i++) { "+ "try { Move-Item -LiteralPath '%s' -Destination '%s' -Force -ErrorAction Stop; $ok=$true; break } "+ "catch { Start-Sleep -Milliseconds 250 } }; "+ "if ($ok) { Start-Process -FilePath '%s' -ArgumentList '--post-update' } "+ "else { Start-Process -FilePath '%s' }", os.Getpid(), q(pending), q(exe), q(exe), q(exe)) cmd := exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps) hideConsole(cmd) if err := cmd.Start(); err != nil { return fmt.Errorf("schedule the update swap: %w", err) } applog.Printf("update: swap scheduled for after exit (%s → %s)", filepath.Base(pending), filepath.Base(exe)) return nil }