From 69743d53bdbe0834bf5bda3f43855a56b5bb0775 Mon Sep 17 00:00:00 2001 From: luotianyi Date: Thu, 8 Oct 2026 00:19:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(turnstile):=20=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=20CDP=20=E7=AD=89=E5=BE=85=20/=20=E5=85=B3=E9=97=AD=E4=B8=8E?= =?UTF-8?q?=20Windows=20=E9=99=8D=E6=9D=83=E5=88=86=E8=A3=82=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --do-not-de-elevate:修复管理员父进程下 Chromium/Edge 自行降权导致的 窗口残留 / CDP 连不上 / 进程孤儿 - DevTools 等待窗口不再把启动器退出当失败;关闭走 CDP Browser.close 兜底 --- internal/turnstile/browser.go | 54 +++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/internal/turnstile/browser.go b/internal/turnstile/browser.go index 2b27eb9..a10cc91 100644 --- a/internal/turnstile/browser.go +++ b/internal/turnstile/browser.go @@ -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 {