publish: 1.1.0 snapshot (a358335)

channel: master
version: 1.1.0
source-ref: master
published-at-utc: 2026-03-29T11:32:04Z
This commit is contained in:
Ant Browser Release Bot
2026-03-29 19:32:05 +08:00
parent fc5a6fadbe
commit 3d264eb83d
58 changed files with 6106 additions and 464 deletions
+245 -11
View File
@@ -1,43 +1,259 @@
package backend
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
const browserStartReadyTimeout = 10 * time.Second
const browserStartStableWindow = 1200 * time.Millisecond
const browserDebugProbeTimeout = 250 * time.Millisecond
func waitBrowserDebugPortReady(debugPort int, timeout time.Duration) error {
var errBrowserDebugPortPending = errors.New("browser debug port pending")
type browserStartupExitError struct {
exitErr error
stderrTail string
}
func (e *browserStartupExitError) Error() string {
detail := e.Detail()
if detail == "" && e.exitErr != nil {
detail = strings.TrimSpace(e.exitErr.Error())
}
if detail == "" {
return "browser process exited before ready"
}
return fmt.Sprintf("browser process exited before ready: %s", detail)
}
func (e *browserStartupExitError) Detail() string {
lines := strings.Split(strings.TrimSpace(e.stderrTail), "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line != "" {
return line
}
}
return ""
}
func newBrowserStartupExitError(result browserProcessExitResult) error {
return &browserStartupExitError{
exitErr: result.Err,
stderrTail: result.StderrTail,
}
}
func waitBrowserDebugPortReady(initialDebugPort int, userDataDir string, timeout time.Duration, monitor *browserProcessMonitor) (int, error) {
deadline := time.Now().Add(timeout)
allowDetachedGrace := initialDebugPort > 0
var lastErr error
var exitResult browserProcessExitResult
exitObserved := false
for time.Now().Before(deadline) {
if canConnectDebugPort(debugPort, 250*time.Millisecond) {
return nil
debugPort, resolveErr := resolveBrowserDebugPort(initialDebugPort, userDataDir, monitor)
if resolveErr == nil {
if err := probeBrowserDebugPort(debugPort, browserDebugProbeTimeout); err == nil {
return debugPort, nil
} else {
lastErr = err
}
} else if !errors.Is(resolveErr, errBrowserDebugPortPending) {
lastErr = resolveErr
}
if monitor != nil && monitor.HasExited() {
if !exitObserved {
exitResult = monitor.Result()
exitObserved = true
if !allowDetachedGrace {
return 0, newBrowserStartupExitError(exitResult)
}
exitDeadline := time.Now().Add(browserLauncherDetachGraceWindow)
if exitDeadline.After(deadline) {
deadline = exitDeadline
}
}
}
time.Sleep(150 * time.Millisecond)
}
if !exitObserved && monitor != nil && monitor.HasExited() {
exitResult = monitor.Result()
exitObserved = true
if !allowDetachedGrace {
return 0, newBrowserStartupExitError(exitResult)
}
postExitDeadline := time.Now().Add(browserLauncherDetachGraceWindow)
for time.Now().Before(postExitDeadline) {
if debugPort, resolveErr := resolveBrowserDebugPort(initialDebugPort, userDataDir, monitor); resolveErr == nil {
if err := probeBrowserDebugPort(debugPort, browserDebugProbeTimeout); err == nil {
return debugPort, nil
}
}
time.Sleep(150 * time.Millisecond)
}
}
if exitObserved {
if debugPort, resolveErr := resolveBrowserDebugPort(initialDebugPort, userDataDir, monitor); resolveErr == nil {
if err := probeBrowserDebugPort(debugPort, browserDebugProbeTimeout); err == nil {
return debugPort, nil
}
}
return 0, newBrowserStartupExitError(exitResult)
}
if lastErr != nil {
if debugPort, resolveErr := resolveBrowserDebugPort(initialDebugPort, userDataDir, monitor); resolveErr == nil {
return 0, fmt.Errorf("浏览器进程未在 %s 内完成启动,调试端口 %d 未就绪:%w", timeout.Round(time.Second), debugPort, lastErr)
}
return 0, fmt.Errorf("浏览器进程未在 %s 内完成启动,尚未获取调试端口:%w", timeout.Round(time.Second), lastErr)
}
return fmt.Errorf("浏览器进程未在 %s 内完成启动,调试端口 %d 未就绪", timeout.Round(time.Second), debugPort)
if debugPort, resolveErr := resolveBrowserDebugPort(initialDebugPort, userDataDir, monitor); resolveErr == nil {
return 0, fmt.Errorf("浏览器进程未在 %s 内完成启动,调试端口 %d 未就绪", timeout.Round(time.Second), debugPort)
}
return 0, fmt.Errorf("浏览器进程未在 %s 内完成启动,尚未获取调试端口", timeout.Round(time.Second))
}
func waitBrowserDebugPortStable(debugPort int, timeout time.Duration, stableFor time.Duration) error {
if err := waitBrowserDebugPortReady(debugPort, timeout); err != nil {
return err
func waitBrowserDebugPortStable(initialDebugPort int, userDataDir string, timeout time.Duration, stableFor time.Duration, monitor *browserProcessMonitor) (int, error) {
debugPort, err := waitBrowserDebugPortReady(initialDebugPort, userDataDir, timeout, monitor)
if err != nil {
return 0, err
}
if stableFor <= 0 {
return nil
return debugPort, nil
}
allowDetachedGrace := initialDebugPort > 0
deadline := time.Now().Add(stableFor)
for time.Now().Before(deadline) {
if !canConnectDebugPort(debugPort, 250*time.Millisecond) {
return fmt.Errorf("浏览器调试端口 %d 短暂就绪后又失效", debugPort)
if monitor != nil && monitor.HasExited() {
if !allowDetachedGrace {
return 0, newBrowserStartupExitError(monitor.Result())
}
}
if err := probeBrowserDebugPort(debugPort, browserDebugProbeTimeout); err != nil {
if monitor != nil && monitor.HasExited() {
if !allowDetachedGrace {
return 0, newBrowserStartupExitError(monitor.Result())
}
}
return 0, fmt.Errorf("浏览器调试端口 %d 短暂就绪后又失效:%w", debugPort, err)
}
time.Sleep(150 * time.Millisecond)
}
return debugPort, nil
}
func resolveBrowserDebugPort(initialDebugPort int, userDataDir string, monitor *browserProcessMonitor) (int, error) {
if initialDebugPort > 0 {
return initialDebugPort, nil
}
if monitor != nil {
if debugPort, ok := monitor.DebugPort(); ok {
return debugPort, nil
}
}
if debugPort, err := readBrowserDebugPortFile(userDataDir); err == nil {
if monitor != nil {
monitor.SetDebugPort(debugPort)
}
return debugPort, nil
} else if !errors.Is(err, errBrowserDebugPortPending) {
return 0, err
}
return 0, errBrowserDebugPortPending
}
func readBrowserDebugPortFile(userDataDir string) (int, error) {
userDataDir = strings.TrimSpace(userDataDir)
if userDataDir == "" {
return 0, errBrowserDebugPortPending
}
data, err := os.ReadFile(filepath.Join(userDataDir, "DevToolsActivePort"))
if err != nil {
if os.IsNotExist(err) {
return 0, errBrowserDebugPortPending
}
return 0, fmt.Errorf("读取 DevToolsActivePort 失败: %w", err)
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) == 0 || strings.TrimSpace(lines[0]) == "" {
return 0, errBrowserDebugPortPending
}
port, err := strconv.Atoi(strings.TrimSpace(lines[0]))
if err != nil || port <= 0 {
return 0, fmt.Errorf("DevToolsActivePort 内容无效: %q", lines[0])
}
return port, nil
}
func probeBrowserDebugPort(debugPort int, requestTimeout time.Duration) error {
if debugPort <= 0 {
return fmt.Errorf("invalid debug port %d", debugPort)
}
client := &http.Client{Timeout: requestTimeout}
versionErr := probeBrowserJSONVersion(client, debugPort)
if versionErr == nil {
return nil
}
listErr := probeBrowserJSONList(client, debugPort)
if listErr == nil {
return nil
}
return fmt.Errorf("%v; %v", versionErr, listErr)
}
func probeBrowserJSONVersion(client *http.Client, debugPort int) error {
var payload struct {
Browser string `json:"Browser"`
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
}
if err := fetchBrowserDebugJSON(client, debugPort, "/json/version", &payload); err != nil {
return err
}
if strings.TrimSpace(payload.Browser) == "" && strings.TrimSpace(payload.WebSocketDebuggerURL) == "" {
return fmt.Errorf("/json/version missing Browser and webSocketDebuggerUrl")
}
return nil
}
func probeBrowserJSONList(client *http.Client, debugPort int) error {
var payload []map[string]interface{}
return fetchBrowserDebugJSON(client, debugPort, "/json/list", &payload)
}
func fetchBrowserDebugJSON(client *http.Client, debugPort int, path string, dest interface{}) error {
url := fmt.Sprintf("http://127.0.0.1:%d%s", debugPort, path)
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("%s request failed: %w", path, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s returned HTTP %d", path, resp.StatusCode)
}
decoder := json.NewDecoder(io.LimitReader(resp.Body, 256*1024))
if err := decoder.Decode(dest); err != nil {
return fmt.Errorf("%s returned invalid JSON: %w", path, err)
}
return nil
}
@@ -85,5 +301,23 @@ func describeChromeProcessStartError(chromeBinaryPath string, err error) string
}
func describeBrowserReadyTimeout(debugPort int, timeout time.Duration) string {
return fmt.Sprintf("实例启动失败:浏览器进程已拉起,但在 %s 内未完成就绪,调试端口 %d 未开启。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", timeout.Round(time.Second), debugPort)
if debugPort <= 0 {
return fmt.Sprintf("实例启动失败:浏览器进程已拉起,但在 %s 内未完成就绪,且未获取到调试端口。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", timeout.Round(time.Second))
}
return fmt.Sprintf("实例启动失败:浏览器进程已拉起,但在 %s 内未完成就绪,调试端口 %d 未就绪。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", timeout.Round(time.Second), debugPort)
}
func describeBrowserReadyFailure(chromeBinaryPath string, debugPort int, timeout time.Duration, err error) string {
var exitErr *browserStartupExitError
if errors.As(err, &exitErr) {
detail := exitErr.Detail()
if detail == "" && exitErr.exitErr != nil {
detail = strings.TrimSpace(exitErr.exitErr.Error())
}
if detail != "" {
return fmt.Sprintf("实例启动失败:浏览器进程在完成就绪前退出。可执行文件:%s。原因:%s。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", chromeBinaryPath, detail)
}
return fmt.Sprintf("实例启动失败:浏览器进程在完成就绪前退出。可执行文件:%s。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", chromeBinaryPath)
}
return describeBrowserReadyTimeout(debugPort, timeout)
}