The updater reports each step (checking, downloading with percent, verifying, installing, restarting). Edges and sites send that progress and their log with their reports; Hammerhead serves it to a dialog that opens when an update starts, and keeps its own last update log on disk across the restart. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const maxUpdateLines = 200
|
|
|
|
// updRun is the progress of one update an edge or site is carrying out, as that agent reported it.
|
|
type updRun struct {
|
|
ID string // the update request it belongs to
|
|
Stage string
|
|
Pct int
|
|
Lines []string
|
|
}
|
|
|
|
// recordUpdateProgress keeps what an agent says about the update it is running. When the agent
|
|
// reports a new version, the update is finished: its log gets a closing line and is then left alone,
|
|
// so the freshly restarted program (which starts a new, empty log) can't overwrite it.
|
|
// Caller holds s.mu.
|
|
func (s *Server) recordUpdateProgress(agentID, reqID, stage string, pct int, lines []string, oldVersion, newVersion string) {
|
|
run := s.updRuns[agentID]
|
|
if run != nil && run.Stage != "done" && oldVersion != "" && newVersion != "" && newVersion != oldVersion {
|
|
run.Lines = append(run.Lines, time.Now().Format("15:04:05")+" Now running "+newVersion)
|
|
run.Stage, run.Pct = "done", 100
|
|
return
|
|
}
|
|
if reqID == "" || (stage == "" && len(lines) == 0) {
|
|
return
|
|
}
|
|
if run != nil && run.ID == reqID && run.Stage == "done" {
|
|
return
|
|
}
|
|
s.updRuns[agentID] = &updRun{ID: reqID, Stage: stage, Pct: pct, Lines: lines}
|
|
}
|
|
|
|
func (s *Server) updateLogView(agentID string, pending bool, msg, version string) map[string]any {
|
|
out := map[string]any{"running": pending, "msg": msg, "version": version, "stage": "", "pct": 0, "lines": []string{}}
|
|
if run := s.updRuns[agentID]; run != nil {
|
|
out["stage"], out["pct"], out["lines"] = run.Stage, run.Pct, run.Lines
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) edgeUpdateLog(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e := s.findEdge(r.PathValue("id"))
|
|
if e == nil {
|
|
fail(w, 404, errors.New("no such edge"))
|
|
return
|
|
}
|
|
writeJSON(w, 200, s.updateLogView(e.ID, e.UpdateID != "", e.UpdateMsg, e.Version))
|
|
}
|
|
|
|
func (s *Server) siteUpdateLog(w http.ResponseWriter, r *http.Request, c *orgCtx) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
st := s.findSite(r.PathValue("id"))
|
|
if st == nil || !s.siteVisible(c, st) {
|
|
fail(w, 404, errors.New("no such site"))
|
|
return
|
|
}
|
|
writeJSON(w, 200, s.updateLogView(st.ID, st.UpdateID != "", st.UpdateMsg, st.Version))
|
|
}
|
|
|
|
// selfUpdate is the progress of this server's own update. It is kept in memory and in <data>/update.log,
|
|
// so the page can still show it after the restart that ends a successful update.
|
|
type selfUpdate struct {
|
|
mu sync.Mutex
|
|
running bool
|
|
stage string
|
|
pct int
|
|
lines []string
|
|
}
|
|
|
|
func (s *Server) updateLogPath() string { return filepath.Join(s.dir, "update.log") }
|
|
|
|
func (s *Server) selfNote(stage string, pct int, line string) {
|
|
u := &s.selfUpd
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
if stage != "" {
|
|
u.stage = stage
|
|
}
|
|
if pct >= 0 {
|
|
u.pct = pct
|
|
}
|
|
if line == "" {
|
|
return
|
|
}
|
|
line = time.Now().Format("15:04:05") + " " + line
|
|
u.lines = append(u.lines, line)
|
|
if len(u.lines) > maxUpdateLines {
|
|
u.lines = u.lines[len(u.lines)-maxUpdateLines:]
|
|
}
|
|
if f, err := os.OpenFile(s.updateLogPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600); err == nil {
|
|
f.WriteString(line + "\n")
|
|
f.Close()
|
|
}
|
|
}
|
|
|
|
func readLines(path string) []string {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
lines := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
|
if len(lines) == 1 && lines[0] == "" {
|
|
return nil
|
|
}
|
|
if len(lines) > maxUpdateLines {
|
|
lines = lines[len(lines)-maxUpdateLines:]
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// finishSelfUpdateLog runs at startup: if the last run ended by restarting into this process, say so.
|
|
func (s *Server) finishSelfUpdateLog() {
|
|
lines := readLines(s.updateLogPath())
|
|
if n := len(lines); n > 0 && strings.Contains(lines[n-1], "restarting") && s.opts.Version != "" {
|
|
if f, err := os.OpenFile(s.updateLogPath(), os.O_APPEND|os.O_WRONLY, 0o600); err == nil {
|
|
f.WriteString(time.Now().Format("15:04:05") + " Hammerhead restarted: now running " + s.opts.Version + "\n")
|
|
f.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateStatus returns the progress and log of this server's own update, for the Settings page.
|
|
func (s *Server) updateStatus(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
u := &s.selfUpd
|
|
u.mu.Lock()
|
|
out := map[string]any{"running": u.running, "stage": u.stage, "pct": u.pct, "lines": append([]string(nil), u.lines...), "version": s.opts.Version}
|
|
u.mu.Unlock()
|
|
if lines, _ := out["lines"].([]string); len(lines) == 0 {
|
|
// nothing in memory: this is a fresh process, so show the last update's log from disk
|
|
if saved := readLines(s.updateLogPath()); len(saved) > 0 {
|
|
stage := "done"
|
|
if strings.Contains(saved[len(saved)-1], "Failed:") {
|
|
stage = "failed"
|
|
}
|
|
out["lines"], out["stage"], out["pct"] = saved, stage, 100
|
|
}
|
|
}
|
|
writeJSON(w, 200, out)
|
|
}
|