From 1264afe40e580312f8371268a05fa44e352dcd1b Mon Sep 17 00:00:00 2001 From: mram Date: Tue, 22 Sep 2026 08:34:47 +0200 Subject: [PATCH] Monitor restarts itself when the service updated its binary on disk --- cmd/gpu-turnstile/main.go | 3 +++ cmd/gpu-turnstile/monitor.go | 41 +++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cmd/gpu-turnstile/main.go b/cmd/gpu-turnstile/main.go index 848054a..a4195db 100644 --- a/cmd/gpu-turnstile/main.go +++ b/cmd/gpu-turnstile/main.go @@ -1034,6 +1034,9 @@ type statusSnapshot struct { UptimeS int64 `json:"uptime_s"` Downstreams []statusDownstream `json:"downstreams"` Lock statusLock `json:"lock"` + // MonitorNote is set client-side (never over the wire) when the + // monitor's own binary differs from the service's version. + MonitorNote string `json:"-"` } // statusProvider assembles the one-line JSON snapshot for CmdStatus. diff --git a/cmd/gpu-turnstile/monitor.go b/cmd/gpu-turnstile/monitor.go index 7de306e..68caf99 100644 --- a/cmd/gpu-turnstile/monitor.go +++ b/cmd/gpu-turnstile/monitor.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "strings" "time" @@ -21,7 +22,10 @@ const ( ) // monitorCommand renders a live status view of the running service, -// refreshed every second from the control channel. Ctrl+C quits. +// refreshed every second from the control channel. When the service +// reports a different version and the executable on disk changed (the +// updater replaced it), the monitor restarts itself onto the new binary. +// Ctrl+C quits. func monitorCommand() int { if !stdoutIsTerminal() { fmt.Fprintln(os.Stderr, "gpu-turnstile: --monitor needs an interactive terminal") @@ -30,12 +34,26 @@ func monitorCommand() int { enableVirtualTerminal() fmt.Print("\x1b[2J") // clear once; frames then redraw in place defer fmt.Print(cReset + "\n") + exe, _ := os.Executable() + var exeStamp time.Time + if st, err := os.Stat(exe); err == nil { + exeStamp = st.ModTime() + } for { frame := renderWaiting() if reply, err := control.Ask(control.CmdStatus); err == nil { if msg, ok := strings.CutPrefix(reply, "OK "); ok { var snap statusSnapshot if json.Unmarshal([]byte(msg), &snap) == nil { + if snap.Version != "" && snap.Version != version { + if exeChanged(exe, exeStamp) { + fmt.Print("\x1b[2J\x1b[H") + fmt.Printf("gpu-turnstile: service updated to %s — restarting the monitor\n", snap.Version) + restartSelf(exe, "--monitor") + return 0 + } + snap.MonitorNote = fmt.Sprintf("note: the service runs %s, this monitor is %s", snap.Version, version) + } frame = renderMonitor(snap, termWidth()) } } @@ -45,6 +63,24 @@ func monitorCommand() int { } } +// exeChanged reports whether the executable on disk was replaced since the +// recorded stamp (the updater swaps it via rename, which changes ModTime). +func exeChanged(exe string, stamp time.Time) bool { + if exe == "" || stamp.IsZero() { + return false + } + st, err := os.Stat(exe) + return err == nil && !st.ModTime().Equal(stamp) +} + +// restartSelf starts a fresh copy of this executable with the given args on +// the same console; the caller exits right after. +func restartSelf(exe string, args ...string) { + cmd := exec.Command(exe, args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + cmd.Start() //nolint:errcheck // best effort: on failure we just exit +} + func renderWaiting() string { return cDim + " gpu-turnstile — waiting for a running service…" + cReset + "\x1b[K\n" } @@ -81,6 +117,9 @@ func renderMonitor(snap statusSnapshot, width int) string { b.WriteString(fmt.Sprintf(" Queue: %s%d image job(s) waiting%s\x1b[K\n", cYellow, snap.Lock.ImageQueue, cReset)) } + if snap.MonitorNote != "" { + b.WriteString(" " + cYellow + snap.MonitorNote + cReset + "\x1b[K\n") + } return b.String() }