Bug bash fixes: hash routing + back button, hub-only pages reset on machine switch, exited terminals reap after 10 min, self-update refuses while busy unless forced, touch-sized controls, power confirm by machine name, down interfaces collapsed and empty modules hidden, load label, offline backoff, cron log newest first, procs empty message, timer timestamp parsing
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
27f9665606
commit
f093b322ee
+22
-3
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
@@ -328,6 +329,20 @@ func (s *Server) removeNode(w http.ResponseWriter, r *http.Request) {
|
||||
// selfUpdate receives a new binary from a hub, checks it runs, installs it over the
|
||||
// current executable (keeping a .prev copy) and restarts the service.
|
||||
func (s *Server) selfUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("force") != "1" {
|
||||
if t, ok := s.tasks.RunningOf("updates"); ok {
|
||||
jsonErr(w, 409, "busy: a package task is running ("+t.Title+"); retry with force to interrupt it")
|
||||
return
|
||||
}
|
||||
attached := 0
|
||||
for _, t := range s.mgr.List() {
|
||||
attached += t.Clients
|
||||
}
|
||||
if attached > 0 {
|
||||
jsonErr(w, 409, fmt.Sprintf("busy: %d terminal client(s) attached; retry with force to drop them", attached))
|
||||
return
|
||||
}
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
@@ -396,10 +411,14 @@ func copyFile(src, dst string) error {
|
||||
|
||||
func (s *Server) updateMachine(w http.ResponseWriter, r *http.Request) {
|
||||
exe, _ := os.Executable()
|
||||
res := s.nodes.Push(r.Context(), r.PathValue("name"), exe, runtime.GOARCH)
|
||||
res := s.nodes.Push(r.Context(), r.PathValue("name"), exe, runtime.GOARCH, r.URL.Query().Get("force") == "1")
|
||||
log.Printf("machines: update %s -> ok=%v %s %s (from %s)", res.Name, res.OK, res.Version, res.Error, clientIP(r))
|
||||
if !res.OK {
|
||||
jsonErr(w, 400, res.Error)
|
||||
code := 400
|
||||
if strings.HasPrefix(res.Error, "busy:") {
|
||||
code = 409
|
||||
}
|
||||
jsonErr(w, code, res.Error)
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, res)
|
||||
@@ -407,7 +426,7 @@ func (s *Server) updateMachine(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) updateAllMachines(w http.ResponseWriter, r *http.Request) {
|
||||
exe, _ := os.Executable()
|
||||
res := s.nodes.PushAll(r.Context(), exe, runtime.GOARCH)
|
||||
res := s.nodes.PushAll(r.Context(), exe, runtime.GOARCH, r.URL.Query().Get("force") == "1")
|
||||
for _, x := range res {
|
||||
log.Printf("machines: update %s -> ok=%v %s %s (from %s)", x.Name, x.OK, x.Version, x.Error, clientIP(r))
|
||||
}
|
||||
|
||||
@@ -499,15 +499,16 @@ func (m *Manager) OutputTail(ctx context.Context, j Job, n int) (string, error)
|
||||
|
||||
// timers: one `systemctl show` over every loaded timer instead of three list commands.
|
||||
func (m *Manager) timers(ctx context.Context) []Timer {
|
||||
out, err := run(ctx, "", "systemctl", "show", "*.timer", "--timestamp=unix", "--no-pager",
|
||||
out, err := run(ctx, "", "systemctl", "show", "*.timer", "--no-pager",
|
||||
"-p", "Id,Description,ActiveState,SubState,UnitFileState,NextElapseUSecRealtime,LastTriggerUSec,Triggers")
|
||||
if err != nil {
|
||||
return []Timer{}
|
||||
}
|
||||
unix := func(v string) int64 { // "@1789711405" (seconds) or "n/a"
|
||||
if strings.HasPrefix(v, "@") {
|
||||
s, _ := strconv.ParseInt(strings.TrimPrefix(v, "@"), 10, 64)
|
||||
return s * 1000
|
||||
unix := func(v string) int64 { // "Sat 2026-09-19 05:47:13 PDT", "n/a" or ""
|
||||
for _, layout := range []string{"Mon 2006-01-02 15:04:05 MST", "2006-01-02 15:04:05 MST", "Mon 2006-01-02 15:04:05"} {
|
||||
if t, err := time.ParseInLocation(layout, v, time.Local); err == nil {
|
||||
return t.UnixMilli()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ type PushResult struct {
|
||||
// Push uploads binPath (normally the hub's own executable) to the node's self-update
|
||||
// endpoint. The node verifies it, installs it and restarts itself. Refused when the
|
||||
// node's architecture differs from ours.
|
||||
func (m *Manager) Push(ctx context.Context, name, binPath, ourArch string) PushResult {
|
||||
func (m *Manager) Push(ctx context.Context, name, binPath, ourArch string, force bool) PushResult {
|
||||
res := PushResult{Name: name}
|
||||
n, ok := m.node(name)
|
||||
if !ok {
|
||||
@@ -328,7 +328,11 @@ func (m *Manager) Push(ctx context.Context, name, binPath, ourArch string) PushR
|
||||
}
|
||||
defer f.Close()
|
||||
st, _ := f.Stat()
|
||||
up, _ := http.NewRequestWithContext(ctx, "POST", n.URL+"/api/self-update", f)
|
||||
target := n.URL + "/api/self-update"
|
||||
if force {
|
||||
target += "?force=1"
|
||||
}
|
||||
up, _ := http.NewRequestWithContext(ctx, "POST", target, f)
|
||||
up.ContentLength = st.Size()
|
||||
up.Header.Set("Authorization", "Bearer "+n.Token)
|
||||
up.Header.Set("Content-Type", "application/octet-stream")
|
||||
@@ -356,7 +360,7 @@ func (m *Manager) Push(ctx context.Context, name, binPath, ourArch string) PushR
|
||||
}
|
||||
|
||||
// PushAll updates every node, in parallel.
|
||||
func (m *Manager) PushAll(ctx context.Context, binPath, ourArch string) []PushResult {
|
||||
func (m *Manager) PushAll(ctx context.Context, binPath, ourArch string, force bool) []PushResult {
|
||||
var list []config.Node
|
||||
m.cfg.View(func(c *config.Config) { list = append(list, c.Nodes...) })
|
||||
out := make([]PushResult, len(list))
|
||||
@@ -365,7 +369,7 @@ func (m *Manager) PushAll(ctx context.Context, binPath, ourArch string) []PushRe
|
||||
wg.Add(1)
|
||||
go func(i int, name string) {
|
||||
defer wg.Done()
|
||||
out[i] = m.Push(ctx, name, binPath, ourArch)
|
||||
out[i] = m.Push(ctx, name, binPath, ourArch, force)
|
||||
}(i, n.Name)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
@@ -130,6 +130,15 @@ func (m *Manager) pump(s *Session) {
|
||||
s.mu.Unlock()
|
||||
_ = s.ptmx.Close()
|
||||
m.changed()
|
||||
// An exited shell stays listed for a while so the exit is visible, then reaps itself.
|
||||
time.AfterFunc(10*time.Minute, func() {
|
||||
m.mu.Lock()
|
||||
cur, ok := m.sessions[s.ID]
|
||||
m.mu.Unlock()
|
||||
if ok && cur == s && !s.running {
|
||||
_ = m.Close(s.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// broadcast requires s.mu. Slow clients are dropped rather than blocking the PTY.
|
||||
|
||||
Reference in New Issue
Block a user