Files
mtproxy_checker/internal/tdlibexec/tdlibexec.go
T

113 lines
2.8 KiB
Go

// 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")
}