Files
CyberStrikeAI/internal/update/binary.go
T
Sycun e80668b2e9 feat: make a restart part of the one-click update, and recover the page by itself
The console now knows what it is running: the status endpoint compares the binary
identity captured at startup (size + nanosecond mtime) with the file on disk, so
an update, a rollback or a CLI run that swapped the binary without a restart is
reported as needsRestart (with the build time) - a state that used to live only in
one job's result and vanished with the process. That state grows a persistent
banner and POST /api/system/update/restart (409 when there is nothing to activate
or a job owns the tree, 400 without a hook), so a forgotten tick is one click
instead of a shell login.

The restart tick defaults to on exactly where it is safe: launchd/systemd leave
markers in the environment (XPC_SERVICE_NAME, INVOCATION_ID/JOURNAL_STREAM) and
the box follows them; an operator's own choice is never second-guessed afterwards.

While the process is down the console becomes a recovery view that probes the
status endpoint every 2 seconds: 200 means the old process still answers, a dead
socket means it is on its way out, and a 401 - only a new process can fail to know
the session - reloads the page back to #system-update. A job poll that dies during
a requested restart is the restart in flight, not a failure to toast.

Also fixed: updateT now asks i18next not to escape interpolated values, because
the console escapes at render time - the apply confirm dialog has been showing
branch names with / ever since they contained a slash, and the new banner
showed dates the same way.

Tests: 7 new console contract tests (36 total: banner, tick default, restart
confirm/refusal, watchdog 200-vs-401, timeout copy, the in-flight poll death) plus
handler tests for the stamp and the endpoint, all mutation-probed; docs describe
the pending-restart state and the self-recovery.
2026-10-07 22:53:32 +08:00

42 lines
1.4 KiB
Go

package update
import (
"os"
"path/filepath"
"time"
)
// BinaryStamp identifies the executable on disk: a rebuild, a rename-swap from an update,
// or a rollback that puts the previous build back all change it. Size plus nanosecond
// mtime is enough to tell two builds apart, and it costs one stat() on a request path -
// hashing the file would not.
type BinaryStamp struct {
Exists bool `json:"exists"`
Size int64 `json:"size,omitempty"`
ModTime time.Time `json:"modTime,omitempty"`
}
// StampBinary reads the install's binary as it is on disk right now. A missing file is a
// legitimate stamp rather than an error: a source checkout has no binary until the first
// update builds one.
func StampBinary(root string, opts Options) BinaryStamp {
info, err := os.Stat(filepath.Join(root, opts.binaryName()))
if err != nil {
return BinaryStamp{}
}
return BinaryStamp{Exists: true, Size: info.Size(), ModTime: info.ModTime()}
}
// Same reports whether two stamps describe the same file. The comparison is deliberately
// stricter than "the disk file is newer": a rollback moves an older build back into place,
// and the process running the newer one still has to restart for the rollback to be real.
func (s BinaryStamp) Same(other BinaryStamp) bool {
if s.Exists != other.Exists {
return false
}
if !s.Exists {
return true
}
return s.Size == other.Size && s.ModTime.Equal(other.ModTime)
}