package ttslaunch import ( "context" "fmt" "io" "log/slog" "net/http" "net/url" "strings" "time" ) // probeBodyLimit 是探活响应的最大读取量:探活只关心"有没有人应答",回应体一律丢弃, // 但也要有界(别被一个话痨服务的无限流拖住)。 const probeBodyLimit = 4 << 10 // httpProbe 用一次 HTTP GET 回答「这个端口上已经有人在应答吗」。 // // 判定口径(刻意宽松):**任何** HTTP 回应都算"在跑",只有传输层错误(连不上 / // 超时 / 连接被拒)才算"没起"。理由是这个服务的根路径回什么状态码并不稳定 // (404/405/200 都可能),而"端口有人说话"已经足以证明不该再拉第二个实例 —— // 误判成"没起"的代价是去起一个抢同一个端口的进程,代价明显更大。 // // `Proxy: nil` 的实际作用范围(复审记录项 1:别高估它):net/http 本来就不给 // **回环 IP** 走代理,所以探 `127.0.0.1` 时这行可有可无(去掉再设 HTTP_PROXY, // 用例照样全绿)。它真正保护的是 `probeUrl` 被配成**主机名**(如 // `http://tts.local:9880`)的情况:那种地址会被环境里的 HTTP_PROXY 接管,从而 // 得到"有人在跑"的假结论。要覆盖这一点得用主机名 probeUrl 写用例。 func httpProbe(url string, timeout time.Duration) probeFunc { client := &http.Client{ Timeout: timeout, Transport: &http.Transport{ Proxy: nil, DisableKeepAlives: true, }, } return func(ctx context.Context) error { reqCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("ttslaunch: build probe request: %w", err) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("ttslaunch: probe %s: %w", url, err) } defer func() { _ = resp.Body.Close() }() _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, probeBodyLimit)) return nil } } // ValidateProbeURL 校验探活地址的形状:必须是**绝对 http(s) URL 且带主机**。 // // 为什么值得单独一道闸(复审 P2-2b):探活地址写错(`127.0.0.1:9880` 漏 scheme、 // 拼错主机)不是"报个错就完了" —— 它会让探活**每次都失败**,于是"没起"永远成立, // 后端每启动一次就拉一次进程。所以调用方应当把非法地址当作"不启用"。 func ValidateProbeURL(raw string) error { trimmed := strings.TrimSpace(raw) u, err := url.Parse(trimmed) if err != nil { return fmt.Errorf("ttslaunch: parse probe url %q: %w", raw, err) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("ttslaunch: probe url %q is not an absolute http(s) url", raw) } if u.Host == "" { return fmt.Errorf("ttslaunch: probe url %q has no host", raw) } return nil } // discardLogger 是没注入日志器时的兜底(绝不 nil 解引用)。 func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }