mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.1.0 source-ref: master published-at-utc: 2026-03-29T11:32:04Z
324 lines
12 KiB
Go
324 lines
12 KiB
Go
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
|
|
|
|
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) {
|
|
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)
|
|
}
|
|
|
|
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(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 debugPort, nil
|
|
}
|
|
allowDetachedGrace := initialDebugPort > 0
|
|
|
|
deadline := time.Now().Add(stableFor)
|
|
for time.Now().Before(deadline) {
|
|
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
|
|
}
|
|
|
|
func canConnectDebugPort(debugPort int, dialTimeout time.Duration) bool {
|
|
if debugPort <= 0 {
|
|
return false
|
|
}
|
|
|
|
address := fmt.Sprintf("127.0.0.1:%d", debugPort)
|
|
conn, err := net.DialTimeout("tcp", address, dialTimeout)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_ = conn.Close()
|
|
return true
|
|
}
|
|
|
|
func describeChromeProcessStartError(chromeBinaryPath string, err error) string {
|
|
raw := strings.TrimSpace(err.Error())
|
|
lower := strings.ToLower(raw)
|
|
|
|
switch {
|
|
case strings.Contains(lower, "access is denied"),
|
|
strings.Contains(lower, "permission denied"),
|
|
strings.Contains(raw, "拒绝访问"):
|
|
return fmt.Sprintf("实例启动失败:系统拒绝启动浏览器进程。可执行文件:%s。请检查文件权限、杀毒软件拦截,或尝试以管理员身份运行。", chromeBinaryPath)
|
|
case strings.Contains(lower, "not a valid win32 application"),
|
|
strings.Contains(raw, "不是有效的 win32 应用程序"),
|
|
strings.Contains(raw, "不是有效的 Win32 应用程序"),
|
|
strings.Contains(raw, "bad exe format"),
|
|
strings.Contains(lower, "exec format error"),
|
|
strings.Contains(lower, "cannot execute binary file"):
|
|
return fmt.Sprintf("实例启动失败:当前浏览器内核与系统/架构不兼容。可执行文件:%s。请确认 Linux 环境使用的是对应架构的 Chrome 内核,而不是 Windows 可执行文件。", chromeBinaryPath)
|
|
case strings.Contains(raw, "系统找不到指定的文件"),
|
|
strings.Contains(lower, "file not found"),
|
|
strings.Contains(lower, "no such file"),
|
|
strings.Contains(lower, "cannot find the file"):
|
|
return fmt.Sprintf("实例启动失败:浏览器可执行文件不存在。可执行文件:%s。请检查内核路径是否正确,或重新下载内核。", chromeBinaryPath)
|
|
case strings.Contains(raw, "目录名称无效"),
|
|
strings.Contains(lower, "directory name is invalid"):
|
|
return fmt.Sprintf("实例启动失败:浏览器工作目录无效。当前目录:%s。请检查内核路径配置是否正确。", chromeBinaryPath)
|
|
default:
|
|
return fmt.Sprintf("实例启动失败:浏览器进程拉起失败。可执行文件:%s。原因:%s。请检查内核文件是否完整、启动参数是否正确,或是否被安全软件拦截。", chromeBinaryPath, raw)
|
|
}
|
|
}
|
|
|
|
func describeBrowserReadyTimeout(debugPort int, timeout time.Duration) string {
|
|
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)
|
|
}
|