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. }