fix(agent): fall back to install record for sing-box core version (item 12)

Locally built sing-box v1.13.21 carries no version stamp and reports
'unknown', leaving the panel core-version column empty. The agent probe
now falls back to /etc/nexusgate/sing-box/version when the binary
reports no version; agent-singbox.sh writes that record (version
without the v prefix) on every successful install path, including the
already-installed early exit. Agent version stays 0.6.8.
This commit is contained in:
aaaamoooo
2026-10-21 10:36:09 +00:00
parent aa2ee62d32
commit bb974e2ed7
4 changed files with 92 additions and 7 deletions
+36 -7
View File
@@ -211,11 +211,12 @@ type engineReport struct {
var singBoxVersionMu sync.Mutex var singBoxVersionMu sync.Mutex
var singBoxVersionCache struct { var singBoxVersionCache struct {
path string path string
mtimeNs int64 mtimeNs int64
size int64 size int64
version string vfMtimeNs int64 // install-record file mtime (part of the cache key)
probed bool version string
probed bool
} }
// parseSingBoxVersion extracts the version token from the first line of // parseSingBoxVersion extracts the version token from the first line of
@@ -233,22 +234,50 @@ func parseSingBoxVersion(out string) string {
return "" return ""
} }
var singBoxVersionRe = regexp.MustCompile(`^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$`)
// installedSingBoxVersion reads the install record written by
// agent-singbox.sh (/etc/nexusgate/sing-box/version, content like
// "1.13.21"). It is the fallback for the locally built sing-box, whose
// binary carries no version stamp and reports "unknown" (round-3 item
// 12). A leading "v" is tolerated and stripped; anything that does not
// look like a version is ignored.
func installedSingBoxVersion() string {
data, err := os.ReadFile(cfg.SingBoxVersionFile)
if err != nil {
return ""
}
v := strings.TrimSpace(string(data))
v = strings.TrimPrefix(v, "v")
if !singBoxVersionRe.MatchString(v) {
return ""
}
return v
}
func singBoxVersion() string { func singBoxVersion() string {
info, err := os.Stat(cfg.SingBoxBin) info, err := os.Stat(cfg.SingBoxBin)
if err != nil { if err != nil {
return "" return ""
} }
var vfMtimeNs int64 = -1
if vfInfo, err := os.Stat(cfg.SingBoxVersionFile); err == nil {
vfMtimeNs = vfInfo.ModTime().UnixNano()
}
singBoxVersionMu.Lock() singBoxVersionMu.Lock()
defer singBoxVersionMu.Unlock() defer singBoxVersionMu.Unlock()
c := &singBoxVersionCache c := &singBoxVersionCache
if c.probed && c.path == cfg.SingBoxBin && c.mtimeNs == info.ModTime().UnixNano() && c.size == info.Size() { if c.probed && c.path == cfg.SingBoxBin && c.mtimeNs == info.ModTime().UnixNano() && c.size == info.Size() && c.vfMtimeNs == vfMtimeNs {
return c.version return c.version
} }
version := "" version := ""
if out, _, code := runCapture(5*time.Second, cfg.SingBoxBin, "version"); code == 0 { if out, _, code := runCapture(5*time.Second, cfg.SingBoxBin, "version"); code == 0 {
version = parseSingBoxVersion(out) version = parseSingBoxVersion(out)
} }
c.path, c.mtimeNs, c.size, c.version, c.probed = cfg.SingBoxBin, info.ModTime().UnixNano(), info.Size(), version, true if version == "" {
version = installedSingBoxVersion()
}
c.path, c.mtimeNs, c.size, c.vfMtimeNs, c.version, c.probed = cfg.SingBoxBin, info.ModTime().UnixNano(), info.Size(), vfMtimeNs, version, true
return version return version
} }
+2
View File
@@ -35,6 +35,7 @@ type config struct {
Root string // NG_CONFIG_DIR Root string // NG_CONFIG_DIR
ResourceDir string // Root + "/resources" ResourceDir string // Root + "/resources"
SingBoxConfig string SingBoxConfig string
SingBoxVersionFile string
SingBoxLog string SingBoxLog string
SingBoxCursor string SingBoxCursor string
KeyFile string KeyFile string
@@ -74,6 +75,7 @@ func loadConfig() config {
Root: root, Root: root,
ResourceDir: root + "/resources", ResourceDir: root + "/resources",
SingBoxConfig: envOr("NG_SINGBOX_CONFIG", "/etc/nexusgate/sing-box/config.json"), SingBoxConfig: envOr("NG_SINGBOX_CONFIG", "/etc/nexusgate/sing-box/config.json"),
SingBoxVersionFile: envOr("NG_SINGBOX_VERSION_FILE", "/etc/nexusgate/sing-box/version"),
SingBoxLog: envOr("NG_SINGBOX_LOG", "/var/log/nexusgate/sing-box-access.log"), SingBoxLog: envOr("NG_SINGBOX_LOG", "/var/log/nexusgate/sing-box-access.log"),
SingBoxCursor: envOr("NG_SINGBOX_CURSOR", "/etc/nexusgate/sing-box-cursor.json"), SingBoxCursor: envOr("NG_SINGBOX_CURSOR", "/etc/nexusgate/sing-box-cursor.json"),
KeyFile: envOr("NG_KEY_FILE", "/etc/nexusgate/keys.json"), KeyFile: envOr("NG_KEY_FILE", "/etc/nexusgate/keys.json"),
+43
View File
@@ -71,6 +71,49 @@ func TestSingBoxVersionProbeCachesByMtime(t *testing.T) {
} }
} }
// Round-3 item 12: when the binary reports "unknown" (the locally built
// sing-box has no version stamp), the probe falls back to the install
// record written by agent-singbox.sh.
func TestSingBoxVersionFallsBackToInstallRecord(t *testing.T) {
dir := t.TempDir()
binPath, _ := writeFakeSingBox(t, dir, "unknown")
recordPath := filepath.Join(dir, "version")
if err := os.WriteFile(recordPath, []byte("1.13.21\n"), 0o644); err != nil {
t.Fatal(err)
}
oldBin, oldRecord := cfg.SingBoxBin, cfg.SingBoxVersionFile
cfg.SingBoxBin, cfg.SingBoxVersionFile = binPath, recordPath
defer func() { cfg.SingBoxBin, cfg.SingBoxVersionFile = oldBin, oldRecord }()
singBoxVersionCache.probed = false
defer func() { singBoxVersionCache.probed = false }()
if got := singBoxVersion(); got != "1.13.21" {
t.Fatalf("fallback probe = %q, want 1.13.21", got)
}
// A stamped binary wins over the record.
writeFakeSingBox(t, dir, "1.13.22")
if got := singBoxVersion(); got != "1.13.22" {
t.Fatalf("stamped probe = %q, want 1.13.22 (record must not shadow it)", got)
}
// A garbage record is ignored.
if err := os.WriteFile(recordPath, []byte("not-a-version\n"), 0o644); err != nil {
t.Fatal(err)
}
writeFakeSingBox(t, dir, "unknown")
if got := singBoxVersion(); got != "" {
t.Fatalf("garbage record: got %q, want empty", got)
}
if got := installedSingBoxVersion(); got != "" {
t.Fatalf("installedSingBoxVersion(garbage) = %q, want empty", got)
}
if err := os.WriteFile(recordPath, []byte("v1.13.21\n"), 0o644); err != nil {
t.Fatal(err)
}
if got := installedSingBoxVersion(); got != "1.13.21" {
t.Fatalf("installedSingBoxVersion(v-prefixed) = %q, want 1.13.21", got)
}
}
func TestSingBoxVersionMissingBinary(t *testing.T) { func TestSingBoxVersionMissingBinary(t *testing.T) {
oldBin := cfg.SingBoxBin oldBin := cfg.SingBoxBin
cfg.SingBoxBin = filepath.Join(t.TempDir(), "does-not-exist") cfg.SingBoxBin = filepath.Join(t.TempDir(), "does-not-exist")
+11
View File
@@ -9,7 +9,16 @@ set -Eeuo pipefail
version='v1.13.21' version='v1.13.21'
target='/usr/local/bin/nexusgate-sing-box' target='/usr/local/bin/nexusgate-sing-box'
stats_target='/usr/local/bin/nexusgate-sing-box-stats' stats_target='/usr/local/bin/nexusgate-sing-box-stats'
# The locally built sing-box carries no version stamp (`sing-box version`
# prints "unknown"), so the agent cannot probe the core version from the
# binary. Record the installed version (without the leading v) for the
# agent's fallback probe — see agent-go installedSingBoxVersion.
write_version_record() {
install -d -m 0700 /etc/nexusgate/sing-box
printf '%s\n' "${version#v}" > /etc/nexusgate/sing-box/version
}
if [[ -x "$target" && -x "$stats_target" && "${2:-}" != '--force' ]]; then if [[ -x "$target" && -x "$stats_target" && "${2:-}" != '--force' ]]; then
write_version_record
printf 'sing-box 已安装:%s(如需重装,运行 ng-agent-singbox install --force)\n' "$target" printf 'sing-box 已安装:%s(如需重装,运行 ng-agent-singbox install --force)\n' "$target"
exit 0 exit 0
fi fi
@@ -38,6 +47,7 @@ try_prebuilt() {
install -m 0755 "$tmp_stats" "${stats_target}.next" install -m 0755 "$tmp_stats" "${stats_target}.next"
mv -f "${target}.next" "$target" mv -f "${target}.next" "$target"
mv -f "${stats_target}.next" "$stats_target" mv -f "${stats_target}.next" "$stats_target"
write_version_record
printf 'sing-box 预编译版安装完成:%s\n' "$target" printf 'sing-box 预编译版安装完成:%s\n' "$target"
return 0 return 0
fi fi
@@ -104,6 +114,7 @@ install -m 0755 "$build_dir/sing-box" "$target.next"
install -m 0755 "$build_dir/stats-query" "$stats_target.next" install -m 0755 "$build_dir/stats-query" "$stats_target.next"
mv -f -- "$target.next" "$target" mv -f -- "$target.next" "$target"
mv -f -- "$stats_target.next" "$stats_target" mv -f -- "$stats_target.next" "$stats_target"
write_version_record
printf 'sing-box 统计版已安装:%s\n' "$target" printf 'sing-box 统计版已安装:%s\n' "$target"
if [[ -f /etc/nexusgate/sing-box/config.json ]]; then if [[ -f /etc/nexusgate/sing-box/config.json ]]; then
if command -v systemctl >/dev/null && [[ -d /run/systemd/system ]]; then if command -v systemctl >/dev/null && [[ -d /run/systemd/system ]]; then