package core import ( "strings" "testing" ) // VNC 一直白屏的真因(2026-10-14 定位)。 // // 生产 unit 加固过,其中 `SystemCallFilter=@system-service` 会挡住 chromium // **独立 GPU 进程**启动所需的系统调用 ⇒ 它起不来 ⇒ chromium 放弃并退出: // // ERROR:gpu_process_host.cc] GPU process launch failed: error_code=1002 // FATAL:gpu_data_manager_impl_private.cc] GPU process isn't usable. Goodbye. // // 整个 chromium 几秒内退出 ⇒ **VNC 上永远白屏**,而插件因为只等 CDP 端口就报 // 「已就绪」,把白屏谎报成成功。 // // 为什么长期没发现:手工在**沙箱外**跑同一条命令每次都成功。定位靠 // `systemd-run` 逐 directive 二分 —— 只有 SystemCallFilter 与 ProtectSystem // 能复现,其余加固项无影响。 // // 修法:`--in-process-gpu`(GPU 逻辑跑在浏览器进程内,不 fork)。 func TestBrowserStackUsesInProcessGPU(t *testing.T) { b, err := readBundledChannels(t) if err != nil { t.Fatal(err) } // ① 必须带 --in-process-gpu,否则在加固环境下浏览器起不来 if !strings.Contains(b, "--in-process-gpu") { t.Fatal("chromium 命令行必须带 --in-process-gpu —— 否则在 " + "SystemCallFilter=@system-service 的加固环境下 GPU 进程起不来、" + "浏览器自杀、VNC 永远白屏(真实事故,2026-10-14)") } // ② 必须给可写的 HOME/XDG:ProtectHome=yes 下 /root 只读, // chromium 建 dconf/ibus 目录会持续刷 permission denied 噪声 for _, k := range []string{"XDG_CACHE_HOME=", "XDG_CONFIG_HOME="} { if !strings.Contains(b, k) { t.Errorf("chromium 命令行应设置 %s(加固环境下 /root 只读会产生噪声日志)", k) } } // ③ **存活探针**必须存在:端口在听 ≠ 浏览器可用。 // 没有它时,"浏览器已死"会被谎报成"已就绪",运维打开 VNC 是白屏, // 还会以为是自己操作的问题。谎报比直接报错更坏。 if !strings.Contains(b, "browserUtil.cdpAlive") { t.Error("起栈后必须做 CDP 存活探针 —— 只等端口就报就绪会把白屏谎报成成功") } if !strings.Contains(b, "browserUtil.readTail") { t.Error("启动失败必须把日志真因带给用户,而不是一句「启动失败」") } // ④ 失败必须回收整组进程(否则每次失败都留一套 Xvfb/x11vnc/websockify) idx := strings.Index(b, "浏览器启动后即退出") if idx < 0 { t.Fatal("应有「浏览器启动后即退出」的失败分支") } if !strings.Contains(b[idx:idx+600], "reapBrowser()") { t.Error("起栈失败必须先回收已起的进程(否则泄漏一套 Xvfb/x11vnc/websockify)") } }