Show a progress bar and live log while Hammerhead, edges and sites update
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>
This commit is contained in:
1 parent
9bd48917c7
commit
2b7cb5dd35
11 files changed
+306
-13
No files matched your search
+152
@@ -0,0 +1,152 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user