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 /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) }