74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
//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 <bin>.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
|
|
}
|