//go:build !windows package updater import ( "context" "fmt" "io" "log/slog" "os" "time" ) // Update fetches the new binary, swaps it in, then exits so systemd // restarts the process under the new binary. The caller should close // the WS connection cleanly (so the server transitions the host to // disconnected immediately rather than waiting for the heartbeat // sweep) before invoking. // // Service-user assumption: the agent runs as root under the // systemd-shipped unit, which can write the binary path directly. // If the agent ever moves to a non-root service user, this breaks — // would need a setuid helper or an out-of-process update service. func Update(ctx context.Context, serverURL string) error { binPath, err := resolveOwnBinary() if err != nil { return err } stage, err := fetch(ctx, serverURL, binPath) if err != nil { return err } if err := swap(stage, binPath); err != nil { return err } slog.Info("agent self-update: binary swapped, exiting for systemd restart", "binary", binPath) // Give logger / WS close-frame a moment to flush, then exit. time.Sleep(200 * time.Millisecond) os.Exit(0) return nil // unreachable } // swap copies the running binary to .old (M1 — keep one revision // back for hand-rolled rollback), then atomic-renames the staged // binary into place. Linux supports rename-while-open so this works // even though the running process holds the source open. func swap(stagePath, binPath string) error { src, err := os.Open(binPath) if err != nil { return fmt.Errorf("open running binary: %w", err) } defer func() { _ = src.Close() }() dst, err := os.OpenFile(binPath+".old", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { return fmt.Errorf("open .old: %w", err) } if _, err := io.Copy(dst, src); err != nil { _ = dst.Close() return fmt.Errorf("copy to .old: %w", err) } if err := dst.Sync(); err != nil { _ = dst.Close() return err } if err := dst.Close(); err != nil { return err } if err := os.Rename(stagePath, binPath); err != nil { return fmt.Errorf("rename .new over running binary: %w", err) } return nil }