真机现象:主人看到 Chrome 窗口内有横幅(他读作「不支持接受的参数」),日志只有 一句「等待调试端口时被取消: context deadline exceeded」,看不出是谁的错。 三件(本片): - 接住子进程 stdout/stderr(有界:前 4 KiB / 上报 12 行,只缓存不输出)⇒ 失败时 打一条带 exe / argv / chrome_output / chrome_output_bytes 的 WARN;成功只记字节数 (不刷屏)。 - 三种启动失败在错误里分开:① 进程立刻退出(profile 被占用)② 等端口超时 ③ 调用方预算耗尽(ctx 已结束 ⇒ **根本不 spawn**)。③ 与 ② 用两个截止时间比大小 区分 —— 顺带修掉一个老缺陷:原来 ctx 一结束就报「等待调试端口时被取消」,而那 几乎总是我们自己的 30s 用满,现被正确归到 ②。 - 降级档不再被**已耗尽**的轮预算连坐:如实报「预算耗尽 ⇒ 未尝试浏览器渲染, 这不是浏览器故障」,**不许把"没试"说成"试了失败"**。plugins/tools/io/bodyfetch.go 一字未动(fetchedBy / browserError 语义原样),只在**发起方**决定要不要发起。 对主人那条横幅的结论(有据,非推测):那是 Chrome 的 **bad-flags 警告栏**,触发者 是 `--disable-blink-features=AutomationControlled` —— 它明确在 Chromium 的 kBadFlags 名单里(bad_flags_prompt.cc 列了 kDisableBlinkFeatures/kEnableBlinkFeatures),本机 Chrome 154 zh-CN 的文案是「您使用的是不受支持的命令行标记:$1。稳定性和安全性会 有所下降。」;而「不支持接受的参数」在 Chrome 的 zh-CN/en-US 资源与二进制里**零命中** ⇒ 是意译。⚠️ 该参数是 design/28 §2 要求的(掩盖 navigator.webdriver)⇒ 横幅是 **设计取舍的视觉副作用**,去掉它才消失(但会暴露自动化特征)。登记待主人裁决。 验收(主会话复跑 + 亲自变异):go build ./... + 两包 vet 绿;browser 与 cmd/cyrene 两包测试绿;我用 -overlay 去掉 cmd.Stderr 接线 ⇒ TestDial_ReportsChromeOutputAndArgvOnLaunchFailure 变红(chrome_output="" chrome_output_bytes=0)。 ⚠️ 未部署:跑着的产物不含本片,要等下次 build + restart。
102 lines
3.4 KiB
Go
102 lines
3.4 KiB
Go
package browser
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// 接住子进程输出的两个上限(AGENTS §10:不出现裸魔法数字)。
|
|
const (
|
|
// chromeOutputMaxBytes 是缓存的**前缀**字节数。为什么留前 N 而不是后 N:Chrome 的
|
|
// 抱怨/崩溃信息总在开头,而尾部的常规日志量最大 —— 留头既省内存又留下原因。
|
|
chromeOutputMaxBytes = 4096
|
|
// chromeOutputMaxLines 是失败上报时最多打出的行数(前 N 行)。
|
|
chromeOutputMaxLines = 12
|
|
)
|
|
|
|
// 本文件只做一件事:**接住浏览器自己说的话**。design/28 §6 坑 5「失败要如实报」
|
|
// 的落地前提是「看得见失败」—— 启动失败时浏览器把原因写在自己的 stdout/stderr 上
|
|
// (参数被拒、profile 锁、崩溃),不接住它,日志里就只剩我们这一侧的猜测。
|
|
//
|
|
// 两条口径:
|
|
//
|
|
// - **有界**:只留前 N 字节 / 上报前 N 行。正常启动时 Chrome 也会往 stderr 写常规
|
|
// 信息,全打进日志就是刷屏;缓存也无上限地吃内存。
|
|
// - **不主动打**:本类型只缓存,**不输出**。成功时只报字节数(Debug),失败时由
|
|
// `localChrome.Dial` 一次性把前 N 行与本次 argv 一起报出来(见 launch.go)。
|
|
type chromeOutput struct {
|
|
mu sync.Mutex
|
|
head []byte
|
|
total int
|
|
truncated bool
|
|
}
|
|
|
|
// Write 实现 io.Writer。stdout 与 stderr 是两条独立的管道,`os/exec` 为它们各起一个
|
|
// 复制协程 ⇒ 必须加锁。它**永不返回错误**:缓存失败不能反过来弄坏子进程的写入。
|
|
func (o *chromeOutput) Write(p []byte) (int, error) {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.total += len(p)
|
|
room := chromeOutputMaxBytes - len(o.head)
|
|
if room <= 0 {
|
|
if len(p) > 0 {
|
|
o.truncated = true
|
|
}
|
|
return len(p), nil
|
|
}
|
|
if len(p) > room {
|
|
o.head = append(o.head, p[:room]...)
|
|
o.truncated = true
|
|
return len(p), nil
|
|
}
|
|
o.head = append(o.head, p...)
|
|
return len(p), nil
|
|
}
|
|
|
|
// totalBytes 报告子进程一共写了多少字节(截断时也照实报,不拿留下的那部分当全部)。
|
|
func (o *chromeOutput) totalBytes() int {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
return o.total
|
|
}
|
|
|
|
// preview 返回失败上报用的「前 N 行」:每行内的空白压成单空格、空行去掉;被截断时
|
|
// 在末尾点明**截断与总字节数**(绝不把半截输出说得像完整输出)。没有输出时返回空串,
|
|
// 调用方据此判断「浏览器一个字都没说」。
|
|
func (o *chromeOutput) preview() string {
|
|
o.mu.Lock()
|
|
raw := truncateRune(o.head)
|
|
truncated := o.truncated
|
|
total := o.total
|
|
o.mu.Unlock()
|
|
|
|
kept := make([]string, 0, chromeOutputMaxLines)
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
line = strings.Join(strings.Fields(line), " ")
|
|
if line == "" {
|
|
continue
|
|
}
|
|
kept = append(kept, line)
|
|
if len(kept) == chromeOutputMaxLines {
|
|
truncated = true
|
|
break
|
|
}
|
|
}
|
|
text := strings.Join(kept, " | ")
|
|
if truncated && text != "" {
|
|
text += " | ...(已截断,共 " + strconv.Itoa(total) + " 字节)"
|
|
}
|
|
return text
|
|
}
|
|
|
|
// truncateRune 去掉尾部可能被切断的半个 UTF-8 字符(按字节截断的代价):最多回退
|
|
// utf8.UTFMax-1 个字节,不因个别非法字节丢掉整段可读输出。
|
|
func truncateRune(raw []byte) string {
|
|
for i := 0; i < utf8.UTFMax-1 && len(raw) > 0 && !utf8.Valid(raw); i++ {
|
|
raw = raw[:len(raw)-1]
|
|
}
|
|
return string(raw)
|
|
}
|