Files
Privatae 2043fa5e9e
ci / fuzz (push) Successful in 3m27s
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 3m39s
bastion 0.62.0
2026-11-02 21:31:58 +00:00

72 lines
3.1 KiB
Go

//go:build windows
package main
import (
"fmt"
"os"
"os/exec"
"strconv"
"golang.org/x/sys/windows"
)
// relaunchAfterUpdate restarts this bastion after a self-update swapped its
// binary — because on Windows nothing else will.
//
// update.go ends a successful swap with os.Exit(0), and its comment says why
// that is enough: "systemd Restart=always re-execs the path, now the new
// binary". True on Linux. On Windows there IS no supervisor — deploy/install.ps1
// installs the exe, puts it on PATH, and registers nothing with the Service
// Control Manager. So every Windows self-update has been a clean, silent
// shutdown: the update applies, the process exits, and the machine stops
// heartbeating until someone notices and starts it by hand.
//
// Measured on a real desktop updating 0.23.0 -> 0.23.1: the heartbeat
// stopped at the moment of the swap and the box went dark. It had just
// been anchored, and because the daemon never came back the anchor never reached
// the host either — the machine looked simply unanchored, with nothing anywhere
// saying a process had exited. That is the worst part: the failure has no
// symptom except absence.
//
// So on Windows we start the new binary ourselves before exiting. DETACHED_PROCESS
// plus CREATE_NEW_PROCESS_GROUP so the child does not die with this console, and
// it is deliberately NOT inheriting our handles — a child holding the parent's
// stdio keeps the invoking terminal from ever returning.
//
// Best-effort, and it must stay that way: if the relaunch cannot start, the
// update itself already succeeded and the binary on disk is the new one. Report
// it and exit rather than pretending, so the operator knows to start it.
//
// NOT AS A SERVICE. A networked install runs as a Windows service
// (sysservice_windows.go), and there the Service Control Manager is the
// supervisor: the exit that follows ends the process without reporting a stop,
// which the manager counts as a failure and answers with a restart of the new
// binary. A detached copy started here would be a second bastion outside the
// manager, racing the one it is about to start.
func relaunchAfterUpdate() {
if runningAsService {
return
}
self, err := os.Executable()
if err != nil {
fmt.Printf("self-update: cannot find my own path to restart (%v) — start it again with `privatae-bastion run`\n", err)
return
}
cmd := exec.Command(self, os.Args[1:]...)
// The new bastion waits for this one to exit before it takes the run
// lock (runholder.go takeRunLock); without this it could find the lock
// still held, refuse as a second copy, and leave the machine with none.
cmd.Env = append(os.Environ(), relaunchOfEnv+"="+strconv.Itoa(os.Getpid()))
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
if err := cmd.Start(); err != nil {
fmt.Printf("self-update: could not restart automatically (%v) — the new binary is installed, start it with `privatae-bastion run`\n", err)
return
}
fmt.Printf("self-update: restarted as pid %d\n", cmd.Process.Pid)
// Do NOT Wait: the child is meant to outlive this process.
_ = cmd.Process.Release()
}