Skip to content

chrome-devtools-cli v2.2.0

Latest

Choose a tag to compare

@aeroxy aeroxy released this 29 Oct 14:26

1. High-Level Summary (TL;DR)

  • Impact: Low. One new command, caffeinate, plus fixes to how the daemon handles a lost browser connection. Nothing was removed or renamed. A command that loses the connection now exits with code 9 instead of 1.
  • Key Changes:
    • ☕ New caffeinate Command: Keeps the daemon's approved browser connection alive until it is lost, checking it every 10 seconds. When the connection dies, it exits with an error that says "Lost the connection to ", so an agent running it in the background knows.
    • 🔌 The Daemon Exits on a Reset Connection: After a macOS sleep reset Chrome's connection, the daemon failed every command with Broken pipe and kept running. It now exits on the first failure, and the next command reconnects.
    • 🛡️ Page Errors Can't End the Daemon: An evaluate exception that happened to mention a WebSocket made the daemon exit as if Chrome were gone, dropping its approved connection. It no longer does.
    • ⏱️ DAEMON_IDLE_TIMEOUT_SECS Is Documented, and 0 Is Fixed: The variable that sets the daemon's idle timeout is now in the README and the skill. 0 falls back to the 5-minute default instead of making every command wait 5 seconds and run without the daemon.

2. Visual Overview (Code & Logic Map)

graph TD
    %% Business Goals
    BG1["Keep an Approved Connection Alive"]:::goal
    BG2["Say When the Connection Dies"]:::goal
    BG3["Keep Dialogs for the Next Command"]:::goal
    BG4["Exit on a Reset Connection"]:::goal
    BG5["Survive Page Errors"]:::goal
    BG6["Treat Idle Timeout 0 as Unset"]:::goal

    %% Files/Modules
    subgraph "src/lib.rs (CLI)"
        L1["caffeinate()"]
        L2["caffeinate_stop_reason()"]
    end

    subgraph "src/commands/executor.rs (Dispatch)"
        E1["execute_command()"]
    end

    subgraph "src/cdp.rs (CDP Client)"
        C1["ping()"]
        C2["read_text() + unread"]
        C3["send_raw_no_wait()"]
        C4["ConnectionLost"]
    end

    subgraph "src/daemon.rs (Daemon)"
        D1["handle_connection()"]
        D2["is_connection_lost()"]
        D3["parse_idle_timeout()"]
    end

    BG1 <--> L1
    BG2 <--> L2
    BG3 <--> C2
    BG4 <--> C3
    BG5 <--> D2
    BG6 <--> D3

    L1 --> E1
    E1 --> C1
    C1 --> C2
    C1 --> C4
    C3 --> C4
    C4 --> D2
    D2 --> D1
    D1 --> L2

    classDef goal fill:#e3f2fd,color:#0d47a1,stroke:#0d47a1,stroke-width:2px;

3. Detailed Change Analysis

☕ caffeinate

  • Component: CLI and Command Dispatch (src/lib.rs, src/commands/executor.rs), CDP Client (src/cdp.rs)
  • What Changed: DAEMON_IDLE_TIMEOUT_SECS only applies to the next daemon, so raising it costs the approval it is meant to save, and nothing reports when the connection dies. caffeinate is a loop in the CLI. Every 10 seconds it sends the daemon a check, which the daemon answers with one Browser.getVersion round trip. Each check restarts the daemon's idle timer, so the daemon and its connection live as long as caffeinate runs. Only its first check starts a daemon when none is running. After that it never starts one, because a new connection can put Chrome's approval prompt up with nobody there to answer it.
  • Behavior:
    Situation What caffeinate does
    The connection is fine Keeps running, silently, until Ctrl-C. The daemon then goes back to its idle timeout.
    Chrome closes or resets the connection Exits with code 9 within 10 seconds: error: [E9] Lost the connection to Chrome after 2h13m (WebSocket error: …). Its daemon has exited and caffeinate has stopped.
    The daemon is gone (kill-daemon, or another command found the connection dead first) Exits with code 6: error: [E6] Lost the connection to Chrome after 2h13m: its daemon is no longer running (…). caffeinate has stopped.
    No daemon is running when it starts Starts one, which may ask for approval, then keeps it alive.
  • Agent guidance: Both exit messages end with the same advice: the next command opens a new connection, which Chrome may ask the human to approve, so an agent should tell the human and not restart caffeinate in a loop.
  • Note: Run it with the same --browser/--user-data-dir/--port flags as your other commands. It keeps only that endpoint's daemon alive. Successful checks are not written to the telemetry log; failed ones are.

💬 Dialogs Survive a Check

  • Component: CDP Client (src/cdp.rs)
  • What Changed: A check reads whatever Chrome has queued. Other commands treat a Page.javascriptDialogOpening event as an error and use it up. Nobody reads a check's errors, so the next command would never learn that an alert is open and could hang behind it. A check sets that event aside in CdpClient::unread, which read_text returns before reading the socket, and leaves the event buffers alone. The next command sees what it would have seen without the check.

🔌 Exiting on a Reset Connection

  • Component: CDP Client and Daemon (src/cdp.rs, src/daemon.rs)
  • What Changed: The daemon exits when Chrome's connection is gone, so that the next command can start a fresh one. It only recognized a lost connection on a failed read. A failed write surfaced as a plain I/O error, and after a reset every command fails on its first write, so the daemon never exited. Each failed command also restarted its idle timer. This was seen after a macOS sleep reset the connection to the chrome://inspect server. A failed write now counts as a lost connection too.
  • Output:
    Command Before After
    First command after a reset error: IO error: Broken pipe (os error 32) error: WebSocket error: … and then Lost the connection to Chrome, so the daemon has exited. Run the command again to reconnect; Chrome may ask you to approve the new connection.
    The command after that The same Broken pipe error, every time Starts a fresh daemon and connects.

🛡️ Lost Connections Recognized by Type

  • Component: Daemon (src/daemon.rs), CDP Client (src/cdp.rs), wait-for and navigation (src/commands/pages.rs, src/commands/navigate.rs)
  • What Changed: The daemon decided Chrome was gone by searching the error text for "WebSocket closed" or "WebSocket error", and evaluate passes the page's exception text through. evaluate "throw new Error('WebSocket closed unexpectedly')" made the daemon exit and print the lost-connection message, so the next command needed a new connection, and possibly a fresh approval. Transport failures are now a ConnectionLost error with the same messages, and the daemon checks for that type, which also sees through context that commands add. wait-for and the navigation load wait used to rebuild errors from their text, which would drop the type, so they add context instead. Their messages are unchanged.

🔢 Exit Code 9 for a Lost Connection

  • Component: Daemon (src/daemon.rs)
  • What Changed: A lost-connection response now carries error code 9 (WebSocketDisconnected), which already existed but was unused. Any command that loses the connection therefore exits with 9 instead of 1. caffeinate relies on the code to tell a dead connection from other failures without matching text.

⏱️ DAEMON_IDLE_TIMEOUT_SECS

  • Component: Daemon (src/daemon.rs), README.md, Agent Skill (skill/chrome-devtools/SKILL.md)
  • What Changed: The variable sets the daemon's idle timeout in seconds (default 300) but was undocumented. 0 made the daemon exit the moment it was idle. Against headless Chrome it exited before serving its first command, so every command waited 5 seconds for it and then fell back to a direct connection of its own, which can mean an approval prompt per command. 0 now falls back to the default, as non-numbers already did.
  • Behavior:
    Value Before After
    0 Every command waited 5 seconds, then ran without the daemon The 5-minute default
    3600 One hour One hour (unchanged)
  • Note: The daemon reads it when it is spawned, so a new value applies to the next daemon, not one already running. To keep a running daemon alive, use caffeinate.

📝 Docs

  • Component: README.md, Agent Skill (skill/chrome-devtools/SKILL.md), AGENTS.md
  • What Changed: The README documents caffeinate and DAEMON_IDLE_TIMEOUT_SECS. The skill tells agents to run caffeinate in the background, with the same connection flags, when the user wants the connection kept open. It also says what to do when it exits (tell the user, and don't restart it in a loop), and to suggest the idle timeout rather than kill the daemon to apply it. AGENTS.md records the rules that keep caffeinate safe.

4. Fixes

  • 🐛 The daemon outlived a reset connection. After a macOS sleep reset Chrome's connection, every command failed with Broken pipe, and the daemon kept running, because each failed command restarted its idle timer.
  • 🐛 A page error could end the daemon. An evaluate exception that mentioned a WebSocket was taken for a lost connection, which dropped the daemon's approved connection.
  • 🐛 DAEMON_IDLE_TIMEOUT_SECS=0 made every command wait 5 seconds and then run without the daemon.

5. Impact & Risk Assessment

⚠️ Breaking Changes

  • None. No flag or command was removed or renamed. The behavior changes below are listed under Upgrade Notes.

📋 Upgrade Notes

  • A daemon that is already running keeps the old behavior. The fixes live in the daemon process, and caffeinate needs a daemon started by 2.2.0. Against an older one it fails with Unknown command: caffeinate. An old daemon exits on its 5-minute idle timeout. kill-daemon ends it sooner, but it drops the daemon's approved Chrome connection, so Chrome may ask for consent again.
  • A lost connection exits with code 9, not 1. A script that checks for exit code 1 there must accept 9.
  • The lost-connection error has a second line saying that the daemon has exited and that rerunning the command reconnects. Anything matching the whole message must allow for it.
  • DAEMON_IDLE_TIMEOUT_SECS=0 now means the 5-minute default. It no longer makes the daemon exit as soon as it is idle.

🔍 Known Limitations

  • caffeinate doesn't keep the computer awake. A sleep can drop the connection. caffeinate then exits and says so, and it never reconnects on its own.
  • The write-failure path after a macOS sleep rests on a unit test. A sleep can't be reproduced in testing, so a fake connection reset covers it. Killing Chrome under a live daemon was verified live.
  • A dialog reported by one command can still stall the next. A command that fails with A javascript dialog is open has used up the dialog's event, and the next page command can hang until the dialog is closed. This was seen with list-pages followed by evaluate. It predates 2.2.0, and caffeinate's checks are built not to trigger it.