113 lines
2.8 KiB
Go
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")
|
|
}
|