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) }