agent: command.update handler + updater package (Linux + Windows)

This commit is contained in:
2026-05-06 21:42:50 +01:00
parent f31f6edde7
commit efed96f67a
8 changed files with 421 additions and 17 deletions
+8 -4
View File
@@ -148,6 +148,7 @@ func run() error {
resticBin: resticBin,
resticVer: snap.ResticVersion,
resticSupportsNoOwnership: resticSupportsNoOwnership,
serverURL: cfg.ServerURL,
secrets: sec,
scheduler: scheduler.New(),
}
@@ -214,6 +215,7 @@ type dispatcher struct {
resticBin string
resticVer string // e.g. "0.17.1"; empty if restic isn't installed yet
resticSupportsNoOwnership bool // captured at startup from `restic restore --help`
serverURL string // base URL of the server (used by the self-update fetch)
secrets *secrets.Store
scheduler *scheduler.Scheduler
@@ -395,10 +397,12 @@ func (d *dispatcher) handle(ctx context.Context, env api.Envelope, tx wsclient.S
"up_kbps", up, "down_kbps", down)
}
case api.MsgAgentUpdateAvail:
var p api.AgentUpdateAvailablePayload
_ = env.UnmarshalPayload(&p)
slog.Info("ws agent: update available", "version", p.LatestVersion, "url", p.PackageURL)
case api.MsgCommandUpdate:
var p api.CommandUpdatePayload
if err := env.UnmarshalPayload(&p); err != nil {
return fmt.Errorf("command.update: %w", err)
}
go d.runUpdate(ctx, p, tx)
default:
slog.Debug("ws agent: ignored message", "type", env.Type)
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"context"
"fmt"
"log/slog"
"time"
"gitea.dcglab.co.uk/steve/restic-manager/internal/agent/updater"
"gitea.dcglab.co.uk/steve/restic-manager/internal/agent/wsclient"
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)
// runUpdate handles a server-dispatched command.update. It logs progress
// via log.stream so the live job page captures pre-restart state, then
// calls the platform updater. On Linux the updater calls os.Exit; on
// Windows it spawns a detached helper and returns, with the agent then
// exiting.
//
// The terminal job state is set by the server, not the agent: success
// is "agent re-hellos with matching version" rather than anything the
// agent itself can assert. The only `job.finished` we send from here is
// on the failure path, before any restart attempt.
func (d *dispatcher) runUpdate(ctx context.Context, p api.CommandUpdatePayload, tx wsclient.Sender) {
logf := func(format string, args ...any) {
line := fmt.Sprintf(format, args...)
slog.Info("ws agent: update: " + line)
env, err := api.Marshal(api.MsgLogStream, "", api.LogStreamLine{
JobID: p.JobID,
TS: time.Now().UTC(),
Stream: api.LogStdout,
Payload: line,
})
if err == nil {
_ = tx.Send(env)
}
}
startedEnv, err := api.Marshal(api.MsgJobStarted, "", api.JobStartedPayload{
JobID: p.JobID,
Kind: api.JobUpdate,
StartedAt: time.Now().UTC(),
})
if err == nil {
_ = tx.Send(startedEnv)
}
logf("fetching new binary from %s", d.serverURL)
if err := updater.Update(ctx, d.serverURL); err != nil {
logf("update failed: %v", err)
finishedEnv, mErr := api.Marshal(api.MsgJobFinished, "", api.JobFinishedPayload{
JobID: p.JobID,
Status: api.JobFailed,
FinishedAt: time.Now().UTC(),
Error: err.Error(),
})
if mErr == nil {
_ = tx.Send(finishedEnv)
}
return
}
// Unreachable on Linux (Update calls os.Exit). On Windows control
// returns here while the detached helper does the swap-and-restart;
// the agent then exits cleanly so SCM hands off.
}