release: v3.1
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -152,11 +153,12 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Duration int `json:"duration"`
|
||||
DiagLevel int `json:"diag_level"`
|
||||
GPUIndices []int `json:"gpu_indices"`
|
||||
Duration int `json:"duration"`
|
||||
DiagLevel int `json:"diag_level"`
|
||||
GPUIndices []int `json:"gpu_indices"`
|
||||
Profile string `json:"profile"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
body.DiagLevel = 1
|
||||
if r.ContentLength > 0 {
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
}
|
||||
@@ -172,11 +174,16 @@ func (h *handler) handleAPISATRun(target string) http.HandlerFunc {
|
||||
Status: TaskPending,
|
||||
CreatedAt: time.Now(),
|
||||
params: taskParams{
|
||||
Duration: body.Duration,
|
||||
DiagLevel: body.DiagLevel,
|
||||
GPUIndices: body.GPUIndices,
|
||||
Duration: body.Duration,
|
||||
DiagLevel: body.DiagLevel,
|
||||
GPUIndices: body.GPUIndices,
|
||||
BurnProfile: body.Profile,
|
||||
DisplayName: body.DisplayName,
|
||||
},
|
||||
}
|
||||
if strings.TrimSpace(body.DisplayName) != "" {
|
||||
t.Name = body.DisplayName
|
||||
}
|
||||
globalQueue.enqueue(t)
|
||||
writeJSON(w, map[string]string{"task_id": t.ID, "job_id": t.ID})
|
||||
}
|
||||
@@ -320,18 +327,21 @@ func (h *handler) handleAPINetworkDHCP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
var result app.ActionResult
|
||||
var err error
|
||||
if req.Interface == "" || req.Interface == "all" {
|
||||
result, err = h.opts.App.DHCPAllResult()
|
||||
} else {
|
||||
result, err = h.opts.App.DHCPOneResult(req.Interface)
|
||||
}
|
||||
result, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) {
|
||||
if req.Interface == "" || req.Interface == "all" {
|
||||
return h.opts.App.DHCPAllResult()
|
||||
}
|
||||
return h.opts.App.DHCPOneResult(req.Interface)
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok", "output": result.Body})
|
||||
writeJSON(w, map[string]any{
|
||||
"status": "ok",
|
||||
"output": result.Body,
|
||||
"rollback_in": int(netRollbackTimeout.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) handleAPINetworkStatic(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -357,12 +367,18 @@ func (h *handler) handleAPINetworkStatic(w http.ResponseWriter, r *http.Request)
|
||||
Gateway: req.Gateway,
|
||||
DNS: req.DNS,
|
||||
}
|
||||
result, err := h.opts.App.SetStaticIPv4Result(cfg)
|
||||
result, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) {
|
||||
return h.opts.App.SetStaticIPv4Result(cfg)
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok", "output": result.Body})
|
||||
writeJSON(w, map[string]any{
|
||||
"status": "ok",
|
||||
"output": result.Body,
|
||||
"rollback_in": int(netRollbackTimeout.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────────
|
||||
@@ -421,6 +437,13 @@ func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request)
|
||||
writeError(w, http.StatusServiceUnavailable, "app not configured")
|
||||
return
|
||||
}
|
||||
h.installMu.Lock()
|
||||
installRunning := h.installJob != nil && !h.installJob.isDone()
|
||||
h.installMu.Unlock()
|
||||
if installRunning {
|
||||
writeError(w, http.StatusConflict, "install to disk is already running")
|
||||
return
|
||||
}
|
||||
t := &Task{
|
||||
ID: newJobID("install-to-ram"),
|
||||
Name: "Install to RAM",
|
||||
@@ -528,6 +551,10 @@ func (h *handler) handleAPIInstallRun(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "device not in install candidate list")
|
||||
return
|
||||
}
|
||||
if globalQueue.hasActiveTarget("install-to-ram") {
|
||||
writeError(w, http.StatusConflict, "install to RAM task is already pending or running")
|
||||
return
|
||||
}
|
||||
|
||||
h.installMu.Lock()
|
||||
if h.installJob != nil && !h.installJob.isDone() {
|
||||
@@ -576,9 +603,11 @@ func (h *handler) handleAPIMetricsStream(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Feed server ring buffers
|
||||
for _, t := range sample.Temps {
|
||||
if t.Name == "CPU" {
|
||||
h.ringCPUTemp.push(t.Celsius)
|
||||
break
|
||||
switch t.Group {
|
||||
case "cpu":
|
||||
h.pushNamedMetricRing(&h.cpuTempRings, t.Name, t.Celsius)
|
||||
case "ambient":
|
||||
h.pushNamedMetricRing(&h.ambientTempRings, t.Name, t.Celsius)
|
||||
}
|
||||
}
|
||||
h.ringPower.push(sample.PowerW)
|
||||
@@ -623,6 +652,23 @@ func (h *handler) handleAPIMetricsStream(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) pushNamedMetricRing(dst *[]*namedMetricsRing, name string, value float64) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
for _, item := range *dst {
|
||||
if item != nil && item.Name == name && item.Ring != nil {
|
||||
item.Ring.push(value)
|
||||
return
|
||||
}
|
||||
}
|
||||
*dst = append(*dst, &namedMetricsRing{
|
||||
Name: name,
|
||||
Ring: newMetricsRing(120),
|
||||
})
|
||||
(*dst)[len(*dst)-1].Ring.push(value)
|
||||
}
|
||||
|
||||
// ── Network toggle ────────────────────────────────────────────────────────────
|
||||
|
||||
const netRollbackTimeout = 60 * time.Second
|
||||
@@ -646,33 +692,14 @@ func (h *handler) handleAPINetworkToggle(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.opts.App.SetInterfaceState(req.Iface, !wasUp); err != nil {
|
||||
if _, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) {
|
||||
err := h.opts.App.SetInterfaceState(req.Iface, !wasUp)
|
||||
return app.ActionResult{}, err
|
||||
}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any existing pending change (rollback it first).
|
||||
h.pendingNetMu.Lock()
|
||||
if h.pendingNet != nil {
|
||||
prev := h.pendingNet
|
||||
prev.mu.Lock()
|
||||
prev.timer.Stop()
|
||||
_ = h.opts.App.SetInterfaceState(prev.iface, prev.wasUp)
|
||||
prev.mu.Unlock()
|
||||
}
|
||||
|
||||
pnc := &pendingNetChange{iface: req.Iface, wasUp: wasUp}
|
||||
pnc.timer = time.AfterFunc(netRollbackTimeout, func() {
|
||||
_ = h.opts.App.SetInterfaceState(req.Iface, wasUp)
|
||||
h.pendingNetMu.Lock()
|
||||
if h.pendingNet == pnc {
|
||||
h.pendingNet = nil
|
||||
}
|
||||
h.pendingNetMu.Unlock()
|
||||
})
|
||||
h.pendingNet = pnc
|
||||
h.pendingNetMu.Unlock()
|
||||
|
||||
newState := "up"
|
||||
if wasUp {
|
||||
newState = "down"
|
||||
@@ -684,6 +711,42 @@ func (h *handler) handleAPINetworkToggle(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) applyPendingNetworkChange(apply func() (app.ActionResult, error)) (app.ActionResult, error) {
|
||||
if h.opts.App == nil {
|
||||
return app.ActionResult{}, fmt.Errorf("app not configured")
|
||||
}
|
||||
|
||||
if err := h.rollbackPendingNetworkChange(); err != nil && err.Error() != "no pending network change" {
|
||||
return app.ActionResult{}, err
|
||||
}
|
||||
|
||||
snapshot, err := h.opts.App.CaptureNetworkSnapshot()
|
||||
if err != nil {
|
||||
return app.ActionResult{}, err
|
||||
}
|
||||
|
||||
result, err := apply()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
pnc := &pendingNetChange{snapshot: snapshot}
|
||||
pnc.timer = time.AfterFunc(netRollbackTimeout, func() {
|
||||
_ = h.opts.App.RestoreNetworkSnapshot(snapshot)
|
||||
h.pendingNetMu.Lock()
|
||||
if h.pendingNet == pnc {
|
||||
h.pendingNet = nil
|
||||
}
|
||||
h.pendingNetMu.Unlock()
|
||||
})
|
||||
|
||||
h.pendingNetMu.Lock()
|
||||
h.pendingNet = pnc
|
||||
h.pendingNetMu.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *handler) handleAPINetworkConfirm(w http.ResponseWriter, _ *http.Request) {
|
||||
h.pendingNetMu.Lock()
|
||||
pnc := h.pendingNet
|
||||
@@ -698,19 +761,30 @@ func (h *handler) handleAPINetworkConfirm(w http.ResponseWriter, _ *http.Request
|
||||
}
|
||||
|
||||
func (h *handler) handleAPINetworkRollback(w http.ResponseWriter, _ *http.Request) {
|
||||
if err := h.rollbackPendingNetworkChange(); err != nil {
|
||||
if err.Error() == "no pending network change" {
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "rolled back"})
|
||||
}
|
||||
|
||||
func (h *handler) rollbackPendingNetworkChange() error {
|
||||
h.pendingNetMu.Lock()
|
||||
pnc := h.pendingNet
|
||||
h.pendingNet = nil
|
||||
h.pendingNetMu.Unlock()
|
||||
if pnc == nil {
|
||||
writeError(w, http.StatusConflict, "no pending network change")
|
||||
return
|
||||
return fmt.Errorf("no pending network change")
|
||||
}
|
||||
pnc.mu.Lock()
|
||||
pnc.timer.Stop()
|
||||
pnc.mu.Unlock()
|
||||
if h.opts.App != nil {
|
||||
_ = h.opts.App.SetInterfaceState(pnc.iface, pnc.wasUp)
|
||||
return h.opts.App.RestoreNetworkSnapshot(pnc.snapshot)
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "rolled back"})
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user