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:
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
// Tests for the sing-box version probe (round-2 item 4): parsing of
|
||||
// `sing-box version` output and the mtime/size-keyed cache that keeps
|
||||
// heartbeats from spawning the binary every round.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSingBoxVersion(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"sing-box version 1.13.21\n\nEnvironment: go1.24.6 linux/amd64\n", "1.13.21"},
|
||||
{"sing-box version 1.13.21", "1.13.21"},
|
||||
{"sing-box version unknown\n", ""},
|
||||
{"", ""},
|
||||
{"garbage output\n", ""},
|
||||
{"sing-box version\n", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := parseSingBoxVersion(c.in); got != c.want {
|
||||
t.Fatalf("parseSingBoxVersion(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeFakeSingBox(t *testing.T, dir, version string) (binPath, countPath string) {
|
||||
t.Helper()
|
||||
binPath = filepath.Join(dir, "fake-sing-box")
|
||||
countPath = filepath.Join(dir, "count")
|
||||
script := "#!/bin/sh\necho x >> \"" + countPath + "\"\necho 'sing-box version " + version + "'\n"
|
||||
if err := os.WriteFile(binPath, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return binPath, countPath
|
||||
}
|
||||
|
||||
func TestSingBoxVersionProbeCachesByMtime(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binPath, countPath := writeFakeSingBox(t, dir, "1.2.3")
|
||||
|
||||
oldBin := cfg.SingBoxBin
|
||||
cfg.SingBoxBin = binPath
|
||||
defer func() { cfg.SingBoxBin = oldBin }()
|
||||
singBoxVersionCache.probed = false
|
||||
defer func() { singBoxVersionCache.probed = false }()
|
||||
|
||||
count := func() int {
|
||||
data, _ := os.ReadFile(countPath)
|
||||
return strings.Count(string(data), "x")
|
||||
}
|
||||
if got := singBoxVersion(); got != "1.2.3" {
|
||||
t.Fatalf("first probe = %q, want 1.2.3", got)
|
||||
}
|
||||
if got := singBoxVersion(); got != "1.2.3" {
|
||||
t.Fatalf("cached probe = %q, want 1.2.3", got)
|
||||
}
|
||||
if n := count(); n != 1 {
|
||||
t.Fatalf("binary executed %d times across two heartbeats, want 1 (cached)", n)
|
||||
}
|
||||
// Replacing the binary (engine install/update) must trigger one re-probe.
|
||||
writeFakeSingBox(t, dir, "1.2.4")
|
||||
if got := singBoxVersion(); got != "1.2.4" {
|
||||
t.Fatalf("probe after binary replacement = %q, want 1.2.4", got)
|
||||
}
|
||||
if n := count(); n != 2 {
|
||||
t.Fatalf("binary executed %d times after replacement, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingBoxVersionMissingBinary(t *testing.T) {
|
||||
oldBin := cfg.SingBoxBin
|
||||
cfg.SingBoxBin = filepath.Join(t.TempDir(), "does-not-exist")
|
||||
defer func() { cfg.SingBoxBin = oldBin }()
|
||||
if got := singBoxVersion(); got != "" {
|
||||
t.Fatalf("missing binary: got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user