package transportcfg import ( "os" "strings" "time" ) type RunCommandTimeoutFunc func(timeout time.Duration, name string, args ...string) (stdout string, stderr string, exitCode int, err error) func ValidateRenderedConfigWithBinary( config map[string]any, run RunCommandTimeoutFunc, timeout time.Duration, binaryCandidates ...string, ) *SingBoxIssue { if len(config) == 0 { return &SingBoxIssue{ Field: "config", Severity: "error", Code: "SINGBOX_RENDER_EMPTY", Message: "rendered config is empty", } } bin, _ := FirstExistingBinaryCandidate(binaryCandidates...) if strings.TrimSpace(bin) == "" { return &SingBoxIssue{ Field: "binary", Severity: "warning", Code: "SINGBOX_BINARY_MISSING", Message: "sing-box binary not found; binary check skipped", } } if run == nil { return &SingBoxIssue{ Field: "binary", Severity: "warning", Code: "SINGBOX_CHECK_RUNNER_MISSING", Message: "command runner is not configured; binary check skipped", } } tmp, err := os.CreateTemp("", "svpn-singbox-check-*.json") if err != nil { return &SingBoxIssue{ Field: "binary", Severity: "warning", Code: "SINGBOX_CHECK_TEMPFILE_FAILED", Message: err.Error(), } } tmpPath := tmp.Name() _ = tmp.Close() defer os.Remove(tmpPath) if err := WriteJSONConfigFile(tmpPath, config); err != nil { return &SingBoxIssue{ Field: "binary", Severity: "warning", Code: "SINGBOX_CHECK_TEMPFILE_FAILED", Message: err.Error(), } } stdout, _, code, runErr := run(timeout, bin, "check", "-c", tmpPath) if runErr == nil && code == 0 { return nil } msg := strings.TrimSpace(stdout) if msg == "" && runErr != nil { msg = runErr.Error() } if msg == "" { msg = "sing-box check failed" } return &SingBoxIssue{ Field: "config", Severity: "error", Code: "SINGBOX_CHECK_FAILED", Message: msg, } }