// Package tdlibexec runs an optional external helper (e.g. Node + TDLib) for proxy verification. // Contract: helper argv = [helperPath, proxyURL]; stdout must contain a JSON line: // // {"ok":true,"error":"","exit_code":0} package tdlibexec import ( "bytes" "context" "encoding/json" "errors" "fmt" "os/exec" "strings" "time" ) // Result is the parsed outcome of a helper invocation. type Result struct { OK bool ExitCode int Error string DurationMs int64 } type jsonLine struct { OK bool `json:"ok"` Error string `json:"error"` ExitCode int `json:"exit_code"` } // Run executes helperPath with a single argument proxyURL and parses the last JSON object from stdout. func Run(ctx context.Context, helperPath, proxyURL string) (Result, error) { start := time.Now() ms := func() int64 { return time.Since(start).Milliseconds() } if strings.TrimSpace(helperPath) == "" { return Result{}, errors.New("empty helper path") } cmd := exec.CommandContext(ctx, helperPath, proxyURL) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() duration := ms() if ctx.Err() != nil { return Result{OK: false, ExitCode: 4, Error: "tdlib helper: " + ctx.Err().Error(), DurationMs: duration}, ctx.Err() } if err == nil { r, perr := parseStdout(stdout.Bytes(), stderr.Bytes(), duration) if perr != nil { return Result{OK: false, ExitCode: 1, Error: perr.Error(), DurationMs: duration}, nil } return r, nil } var ee *exec.ExitError if errors.As(err, &ee) { if r, perr := parseStdout(stdout.Bytes(), stderr.Bytes(), duration); perr == nil { if r.ExitCode == 0 && !r.OK { r.ExitCode = ee.ExitCode() } if r.ExitCode == 0 && !r.OK { r.ExitCode = 2 } return r, nil } msg := strings.TrimSpace(stdout.String()) if msg == "" { msg = strings.TrimSpace(stderr.String()) } if msg == "" { msg = err.Error() } code := ee.ExitCode() if code < 0 || code > 4 { code = 2 } return Result{OK: false, ExitCode: code, Error: msg, DurationMs: duration}, nil } return Result{}, fmt.Errorf("tdlib helper: %w", err) } func parseStdout(stdout, stderr []byte, duration int64) (Result, error) { lines := strings.Split(string(stdout), "\n") for i := len(lines) - 1; i >= 0; i-- { line := strings.TrimSpace(lines[i]) if line == "" || line[0] != '{' { continue } var j jsonLine if err := json.Unmarshal([]byte(line), &j); err != nil { continue } if j.ExitCode == 0 && !j.OK { j.ExitCode = 2 } if j.Error == "" && !j.OK { j.Error = "tdlib reported failure" } return Result{OK: j.OK, ExitCode: j.ExitCode, Error: j.Error, DurationMs: duration}, nil } if len(stderr) > 0 { return Result{}, fmt.Errorf("no json in stdout: stderr=%s", strings.TrimSpace(string(stderr))) } return Result{}, fmt.Errorf("no json line in helper stdout") }