fix(turnstile): 浏览器 CDP 等待 / 关闭与 Windows 降权分裂修复

- --do-not-de-elevate:修复管理员父进程下 Chromium/Edge 自行降权导致的
  窗口残留 / CDP 连不上 / 进程孤儿
- DevTools 等待窗口不再把启动器退出当失败;关闭走 CDP Browser.close 兜底
This commit is contained in:
luotianyi committed 2026-10-08 00:19:41 +08:00
1 parent 97a59a2dfb
commit 69743d53bd
1 file changed
+49 -5
+49 -5
View File
@@ -938,6 +938,10 @@ func launchBrowser(ctx context.Context, exe string, cfg config.Turnstile, locale
"--user-data-dir=" + profile,
"--no-first-run",
"--no-default-browser-check",
// Chromium/Edge re-launch themselves de-elevated when the parent runs
// as Administrator, orphaning the real browser and breaking CDP;
// keep the browser inside our process tree instead.
"--do-not-de-elevate",
"--disable-blink-features=AutomationControlled",
"--disable-background-networking",
"--disable-sync",
@@ -989,13 +993,12 @@ func (b *browserProc) waitDevTools(ctx context.Context) error {
deadline := time.Now().Add(30 * time.Second)
var lastErr error
for time.Now().Before(deadline) {
select {
case <-b.exited:
return fmt.Errorf("browser exited early (%v): %s", b.waitErr, b.stderr.String())
case <-ctx.Done():
if ctx.Err() != nil {
return ctx.Err()
default:
}
// A Windows browser launcher may hand off to a longer-lived process
// and exit 0; that is not a failure while the DevTools endpoint is
// still coming up, so the exit check happens only at timeout.
resp, err := client.Get(endpoint)
if err == nil {
body, _ := io.ReadAll(resp.Body)
@@ -1015,10 +1018,17 @@ func (b *browserProc) waitDevTools(ctx context.Context) error {
return ctx.Err()
}
}
select {
case <-b.exited:
return fmt.Errorf("browser exited early (%v): %s", b.waitErr, b.stderr.String())
default:
}
return fmt.Errorf("devtools never came up: %v; stderr: %s", lastErr, b.stderr.String())
}
// Close kills the browser if it is still alive and drops the temp profile.
// On Windows the spawned launcher may have exited while the real browser
// lives on, so a DevTools close is attempted before giving up.
func (b *browserProc) Close() {
select {
case <-b.exited:
@@ -1031,6 +1041,7 @@ func (b *browserProc) Close() {
case <-time.After(3 * time.Second):
}
}
b.closeViaDevTools(2 * time.Second)
if b.tempDir != "" {
for i := 0; i < 3; i++ {
if err := os.RemoveAll(b.tempDir); err == nil {
@@ -1041,6 +1052,39 @@ func (b *browserProc) Close() {
}
}
// closeViaDevTools asks the browser to shut down over the DevTools endpoint.
// It is the only handle left when the original launcher process has exited.
func (b *browserProc) closeViaDevTools(timeout time.Duration) {
ws := b.wsURL
if ws == "" {
client := &http.Client{Timeout: timeout}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/json/version", b.port))
if err != nil {
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
var info struct {
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
}
if json.Unmarshal(body, &info) != nil || info.WebSocketDebuggerURL == "" {
return
}
ws = info.WebSocketDebuggerURL
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := dialCDP(ctx, ws, fmt.Sprintf("http://127.0.0.1:%d", b.port))
if err != nil {
return
}
defer conn.Close()
_, _ = conn.call(ctx, "", "Browser.close", nil)
}
func freePort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {