feat(agent+panel): sing-box core version probe and display (round-2 #4)
agent-go: engineReport gains singBoxVersion. The probe runs
`sing-box version` once, parses the version token from its first line
('unknown' from unstamped builds reports empty), and caches it keyed by
the binary's mtime+size — re-probed only when an engine install/update
replaces the file. Steady-state heartbeats pay one stat, no subprocess.
Agent version stays 0.6.8.
panel-go: EngineState.singBoxVersion persisted from heartbeats (old
agents without the field keep showing '-'), servers table core-version
column now renders it. Golden gains heartbeatWithEngine +
serversAfterEngineHeartbeat sections locking the full chain, plus a
listen-address (<hostip>) normalization for the sandbox bind address.
singboxversion_test.go covers parsing, cache hits, and re-probe on
binary replacement.
This commit is contained in:
@@ -199,6 +199,57 @@ type engineReport struct {
|
||||
SingBoxInstalled bool `json:"singBoxInstalled"`
|
||||
Certificates []string `json:"certificates"`
|
||||
SingBoxStatus string `json:"singBoxStatus"`
|
||||
SingBoxVersion string `json:"singBoxVersion,omitempty"`
|
||||
}
|
||||
|
||||
// ---- sing-box version probe -------------------------------------------------
|
||||
// The core version is probed by running `sing-box version` exactly once and
|
||||
// cached against the binary's mtime+size. It is re-probed only when the file
|
||||
// changes (an engine install/update replaces the binary) — the first probe
|
||||
// happens on the first heartbeat after agent start. Steady-state heartbeats
|
||||
// pay one stat, never a subprocess.
|
||||
|
||||
var singBoxVersionMu sync.Mutex
|
||||
var singBoxVersionCache struct {
|
||||
path string
|
||||
mtimeNs int64
|
||||
size int64
|
||||
version string
|
||||
probed bool
|
||||
}
|
||||
|
||||
// parseSingBoxVersion extracts the version token from the first line of
|
||||
// `sing-box version` output ("sing-box version 1.13.21"). Unstamped local
|
||||
// builds print "unknown"; those report no version.
|
||||
func parseSingBoxVersion(out string) string {
|
||||
line := out
|
||||
if i := strings.IndexByte(line, '\n'); i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 3 && fields[0] == "sing-box" && fields[1] == "version" && fields[2] != "unknown" {
|
||||
return fields[2]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func singBoxVersion() string {
|
||||
info, err := os.Stat(cfg.SingBoxBin)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
singBoxVersionMu.Lock()
|
||||
defer singBoxVersionMu.Unlock()
|
||||
c := &singBoxVersionCache
|
||||
if c.probed && c.path == cfg.SingBoxBin && c.mtimeNs == info.ModTime().UnixNano() && c.size == info.Size() {
|
||||
return c.version
|
||||
}
|
||||
version := ""
|
||||
if out, _, code := runCapture(5*time.Second, cfg.SingBoxBin, "version"); code == 0 {
|
||||
version = parseSingBoxVersion(out)
|
||||
}
|
||||
c.path, c.mtimeNs, c.size, c.version, c.probed = cfg.SingBoxBin, info.ModTime().UnixNano(), info.Size(), version, true
|
||||
return version
|
||||
}
|
||||
|
||||
// engineHealth mirrors agent.js engineHealth(). Callers must hold stateMu
|
||||
@@ -214,6 +265,7 @@ func engineHealthLocked(resources []map[string]interface{}) engineReport {
|
||||
if installed {
|
||||
result.Status = service.Status
|
||||
result.Detail = service.Detail
|
||||
result.SingBoxVersion = singBoxVersion()
|
||||
} else {
|
||||
result.Status = "error"
|
||||
result.Detail = "sing-box 及统计组件未安装;请在入口机器运行 ng-agent engine install"
|
||||
|
||||
Reference in New Issue
Block a user