From bb974e2ed7b1d64d5200667c98bf7d47c0a7433a Mon Sep 17 00:00:00 2001 From: aaaamoooo Date: Wed, 21 Oct 2026 10:36:09 +0000 Subject: [PATCH] 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. --- agent-go/health.go | 43 +++++++++++++++++++++++++++------ agent-go/main.go | 2 ++ agent-go/singboxversion_test.go | 43 +++++++++++++++++++++++++++++++++ scripts/agent-singbox.sh | 11 +++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/agent-go/health.go b/agent-go/health.go index e8ad46c..7df0771 100644 --- a/agent-go/health.go +++ b/agent-go/health.go @@ -211,11 +211,12 @@ type engineReport struct { var singBoxVersionMu sync.Mutex var singBoxVersionCache struct { - path string - mtimeNs int64 - size int64 - version string - probed bool + path string + mtimeNs int64 + size int64 + vfMtimeNs int64 // install-record file mtime (part of the cache key) + version string + probed bool } // parseSingBoxVersion extracts the version token from the first line of @@ -233,22 +234,50 @@ func parseSingBoxVersion(out string) string { 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 { info, err := os.Stat(cfg.SingBoxBin) if err != nil { return "" } + var vfMtimeNs int64 = -1 + if vfInfo, err := os.Stat(cfg.SingBoxVersionFile); err == nil { + vfMtimeNs = vfInfo.ModTime().UnixNano() + } singBoxVersionMu.Lock() defer singBoxVersionMu.Unlock() 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 } 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 + 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 } diff --git a/agent-go/main.go b/agent-go/main.go index 3d0bb67..d79eee4 100644 --- a/agent-go/main.go +++ b/agent-go/main.go @@ -35,6 +35,7 @@ type config struct { Root string // NG_CONFIG_DIR ResourceDir string // Root + "/resources" SingBoxConfig string + SingBoxVersionFile string SingBoxLog string SingBoxCursor string KeyFile string @@ -74,6 +75,7 @@ func loadConfig() config { Root: root, ResourceDir: root + "/resources", 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"), SingBoxCursor: envOr("NG_SINGBOX_CURSOR", "/etc/nexusgate/sing-box-cursor.json"), KeyFile: envOr("NG_KEY_FILE", "/etc/nexusgate/keys.json"), diff --git a/agent-go/singboxversion_test.go b/agent-go/singboxversion_test.go index 4187ad3..395d435 100644 --- a/agent-go/singboxversion_test.go +++ b/agent-go/singboxversion_test.go @@ -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) { oldBin := cfg.SingBoxBin cfg.SingBoxBin = filepath.Join(t.TempDir(), "does-not-exist") diff --git a/scripts/agent-singbox.sh b/scripts/agent-singbox.sh index 22aa6a8..d59f859 100644 --- a/scripts/agent-singbox.sh +++ b/scripts/agent-singbox.sh @@ -9,7 +9,16 @@ set -Eeuo pipefail version='v1.13.21' target='/usr/local/bin/nexusgate-sing-box' 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 + write_version_record printf 'sing-box 已安装:%s(如需重装,运行 ng-agent-singbox install --force)\n' "$target" exit 0 fi @@ -38,6 +47,7 @@ try_prebuilt() { install -m 0755 "$tmp_stats" "${stats_target}.next" mv -f "${target}.next" "$target" mv -f "${stats_target}.next" "$stats_target" + write_version_record printf 'sing-box 预编译版安装完成:%s\n' "$target" return 0 fi @@ -104,6 +114,7 @@ install -m 0755 "$build_dir/sing-box" "$target.next" install -m 0755 "$build_dir/stats-query" "$stats_target.next" mv -f -- "$target.next" "$target" mv -f -- "$stats_target.next" "$stats_target" +write_version_record printf 'sing-box 统计版已安装:%s\n' "$target" if [[ -f /etc/nexusgate/sing-box/config.json ]]; then if command -v systemctl >/dev/null && [[ -d /run/systemd/system ]]; then