Skip to content

chrome-devtools-cli v2.0.0

Latest

Choose a tag to compare

@aeroxy aeroxy released this 30 Aug 03:04

1. High-Level Summary (TL;DR)

  • Impact: High — introduces native support for Microsoft Edge, restructures the background daemon to support multiple concurrent browser sessions, and fixes a bug in which kill-daemon could signal an unrelated process.
  • Key Changes:
    • Microsoft Edge Support: Added the --browser edge flag to connect to Edge profiles across macOS, Linux, and Windows.
    • 🏗️ Multi-Daemon Architecture: Shifted from a single global daemon per user to one daemon per browser endpoint (keyed by a hash of the WebSocket URL).
    • 🛠️ New CLI Commands: Added list-daemons to inspect daemon state, and an --all flag on kill-daemon to sweep every daemon.
    • 🔒 Signal Safety: kill-daemon now proves a PID still belongs to a live daemon before sending SIGTERM. Previously a recycled PID could be signalled.
    • 📝 Enhanced Diagnostics: Error messages name the targeted browser (e.g. "Failed to connect to Microsoft Edge") instead of hardcoding "Chrome".

2. Visual Overview (Code & Logic Map)

graph TD
    %% Business Goals
    BG1["Support Multiple Concurrent Browsers"]:::goal
    BG2["Microsoft Edge Support"]:::goal
    BG3["Manage Daemon Instances"]:::goal
    BG4["Never Signal a Foreign Process"]:::goal

    %% Files/Modules
    subgraph "src/protocol.rs (IPC & Registry)"
        P1["instance_key(ws_url)"]
        P2["enumerate_instance_keys()"]
        P3["info_path(key)"]
        P4["lock_path()"]
    end

    subgraph "src/browser.rs (Resolution)"
        B1["Browser::parse()"]
        B2["resolve_ws_url()"]
        B3["default_user_data_dir()"]
    end

    subgraph "src/lib.rs & src/daemon.rs (Execution)"
        L1["print_daemon_list()"]
        L2["stop_daemon_at()"]
        L3["daemon_listening_at()"]
        D1["run_daemon(ws_url, browser)"]
        D2["write_info_file()"]
    end

    BG1 <--> P1
    BG1 <--> D1

    BG2 <--> B1
    BG2 <--> B2
    BG2 <--> B3

    BG3 <--> L1
    BG3 <--> L2
    BG3 <--> P2

    BG4 <--> L3
    BG4 <--> P4

    P1 --> D1
    P2 --> L1
    P2 --> L2
    D1 --> D2
    D2 --> P3
    L1 --> P3
    L2 --> L3
    P4 --> L2
    P4 --> D1

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

3. Detailed Change Analysis

🌐 Browser Support & Resolution

  • Component: Browser Resolution (src/browser.rs)
  • What Changed: Introduced the Browser enum to handle browser-specific profile path resolution. resolve_ws_url now takes a browser argument, so the CLI locates DevToolsActivePort for Chrome or Edge per OS. Browser and channel names are matched case-insensitively and trimmed; errors quote the name as typed.
  • New Configuration Flags:
    Flag / Env Var Default Description
    --browser / CHROME_BROWSER chrome Target browser. Accepts chrome, edge, msedge, any casing.
  • Edge profile locations:
    OS Path
    macOS ~/Library/Application Support/Microsoft Edge/
    Linux ~/.config/microsoft-edge/
    Windows %LOCALAPPDATA%\Microsoft\Edge\User Data\
  • Note: --channel composes with --browser (--browser edge --channel beta). Edge ships no Canary for Linux, so that combination is rejected rather than resolved to a directory that cannot exist.

🏗️ Daemon Multi-Instance Architecture

  • Component: Daemon Registry & IPC (src/protocol.rs, src/daemon.rs)

  • What Changed: Daemons are scoped to the browser endpoint via a 16-hex-digit FNV-1a hash of the resolved WebSocket URL (instance_key). A daemon attached to a regular Chrome window can no longer intercept commands intended for a headless Edge instance — previously the single daemon bound to whichever browser the first command resolved and silently ignored later --browser / --user-data-dir flags.

  • Discovery: the filename prefix is the registry. enumerate_instance_keys lists instances by scanning the temp directory for <prefix>-<key>.pid and stripping the prefix; there is no separate index to drift out of sync. The .info sidecar is read afterwards, per key, and is optional — a daemon without one still lists, with ? columns.

  • Session scoping: the URL's browser GUID changes on every browser launch, so a restarted browser gets a fresh daemon instead of inheriting a dead connection. The orphan exits on its 5-minute idle timeout.

  • Daemon Files (Unix names; on Windows %TEMP% is already per-user so there is no uid segment, and the endpoint is -<key>.addr recording a loopback TCP address):

    File Type Old Path New Path Description
    Socket …-daemon-<uid>.sock …-daemon-<uid>-<key>.sock IPC socket per endpoint
    PID …-daemon-<uid>.pid …-daemon-<uid>-<key>.pid Process ID per endpoint
    Info N/A …-daemon-<uid>-<key>.info JSON metadata: browser, ws_url, pid, started_unix
    Lock …-daemon-<uid>.lock …-daemon-<uid>.lock Deliberately not keyed — one lock serializes all startups

    The lock stays shared because it only covers the brief write-PID-then-bind window; a per-instance lock would accumulate a never-removed file per browser session.

🛠️ CLI Commands & Process Management

  • Component: CLI Execution (src/lib.rs, src/commands/executor.rs)
  • Command Behavior Changes:
    Command Old Behavior New Behavior
    list-daemons N/A Lists every daemon with on-disk state — running ones and, on Unix, stale entries whose process is gone — with PID, browser, endpoint and uptime. Supports --json and --toon.
    kill-daemon Kills the global user daemon. Kills only the daemon for the resolved target endpoint.
    kill-daemon --all N/A Sweeps every daemon for the current user, including legacy unkeyed ones. Needs no endpoint.
  • Details: rows are ordered newest-first (entries without a sidecar sort last); list-daemons reads only on-disk state, so it works when every browser has exited. On Windows liveness is not probed, so every row's state is ? and stale never appears.

🗣️ Protocol & Diagnostics

  • Component: CDP Client (src/cdp.rs, src/client.rs)
  • What Changed: The browser display name is threaded into CdpClient::connect, so both the connection failure and the connect timeout name the right browser. The timeout previously told every user to "check Chrome" for a pending consent dialog — misleading under --browser edge, since it pointed at the wrong window.

4. Fixes

  • 🔒 kill-daemon could signal an unrelated process. A daemon killed with SIGKILL leaves its PID file behind (cleanup is skipped by design); the OS may then recycle that PID for another process owned by the same user, and kill-daemon signalled it blind. --all widened the exposure by walking every PID file. stop_daemon_at now confirms a live listener on the daemon's own socket before signalling, and removes the files without signalling when nothing answers.
  • 🔒 TOCTOU between the PID read and the liveness probe. The check above is only sound if no daemon can start in between — otherwise the probe sees a newcomer's listener while the PID in hand is the old, recycled one. The read, probe and signal now run under the same startup lock that covers the daemon's write-PID-then-bind sequence.
  • 🐛 list-daemons could report a dead daemon as running. PID parsing accepted 0, and kill(0, 0) probes the caller's own process group and succeeds. It now uses the same validation kill-daemon applies before signalling.
  • 🐛 --toon was ignored by list-daemons, which fell through to the text table.
  • 🐛 kill-daemon --all printed "No daemons running." before sweeping legacy files, so a legacy-only machine saw that message immediately followed by a daemon being stopped.
  • 🐛 wss:// endpoints rendered as the bare scheme wss: in the list-daemons endpoint column.

5. Impact & Risk Assessment

⚠️ Breaking Changes

  • kill-daemon is now scoped. Scripts relying on a bare kill-daemon to stop all background processes now stop only the daemon for the default (or explicitly passed) profile. Use kill-daemon --all for a global sweep.
  • kill-daemon can now fail where it previously succeeded. A scoped kill must resolve its target endpoint, so if DevToolsActivePort is unreadable — the browser has already exited, or the profile is new — it exits non-zero instead of doing nothing. Cleanup traps running a bare kill-daemon under set -e will abort. Use || true, or --all, which needs no endpoint.
  • A daemon whose socket file was deleted by hand is no longer killable and instead exits on its 5-minute idle timeout. This is the deliberate trade for never signalling a recycled PID.

📋 Upgrade Notes

  • A daemon left running by a pre-key version has an unkeyed PID file that only kill-daemon --all sweeps. Run it once after upgrading, or wait out the idle timeout.
  • Pass --browser even with --ws-endpoint / --user-data-dir. Neither needs it to connect, but --browser is also the label recorded for the daemon and the browser named in connection errors. Without it both say Chrome, whatever the endpoint actually reaches.
  • Environment variables keep the CHROME_ prefix (CHROME_BROWSER, CHROME_USER_DATA_DIR) when targeting Edge. No EDGE_* aliases exist.
  • Temp-directory file count grows with the number of concurrent endpoints (three files per daemon). All are removed on clean exit, on panic, and on SIGTERM/SIGINT.

🔍 Known Limitations

  • On Windows, kill-daemon remains unsupported: it reports that and exits without signalling or removing files. Take the PID from list-daemons and use taskkill /PID <pid>.
  • Enterprise-managed Edge can have remote debugging disabled by policy, in which case DevToolsActivePort never appears. Same failure mode as Chrome under the equivalent policy, but more common on managed fleets.
  • A temp profile is not automatically an isolated one in Edge: its first-run import can pull open tabs and extensions from the default browser, so a scratch profile may come up holding a real signed-in session. Confirm with list-pages before assuming isolation.