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-daemoncould signal an unrelated process. - Key Changes:
- ✨ Microsoft Edge Support: Added the
--browser edgeflag 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-daemonsto inspect daemon state, and an--allflag onkill-daemonto sweep every daemon. - 🔒 Signal Safety:
kill-daemonnow proves a PID still belongs to a live daemon before sendingSIGTERM. 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".
- ✨ Microsoft Edge Support: Added the
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
Browserenum to handle browser-specific profile path resolution.resolve_ws_urlnow takes a browser argument, so the CLI locatesDevToolsActivePortfor 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_BROWSERchromeTarget 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:
--channelcomposes 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-dirflags. -
Discovery: the filename prefix is the registry.
enumerate_instance_keyslists instances by scanning the temp directory for<prefix>-<key>.pidand stripping the prefix; there is no separate index to drift out of sync. The.infosidecar 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>.addrrecording a loopback TCP address):File Type Old Path New Path Description Socket …-daemon-<uid>.sock…-daemon-<uid>-<key>.sockIPC socket per endpoint PID …-daemon-<uid>.pid…-daemon-<uid>-<key>.pidProcess ID per endpoint Info N/A …-daemon-<uid>-<key>.infoJSON metadata: browser,ws_url,pid,started_unixLock …-daemon-<uid>.lock…-daemon-<uid>.lockDeliberately 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-daemonsN/A Lists every daemon with on-disk state — running ones and, on Unix, staleentries whose process is gone — with PID, browser, endpoint and uptime. Supports--jsonand--toon.kill-daemonKills the global user daemon. Kills only the daemon for the resolved target endpoint. kill-daemon --allN/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-daemonsreads only on-disk state, so it works when every browser has exited. On Windows liveness is not probed, so every row's state is?andstalenever 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-daemoncould signal an unrelated process. A daemon killed withSIGKILLleaves its PID file behind (cleanup is skipped by design); the OS may then recycle that PID for another process owned by the same user, andkill-daemonsignalled it blind.--allwidened the exposure by walking every PID file.stop_daemon_atnow 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-daemonscould report a dead daemon as running. PID parsing accepted0, andkill(0, 0)probes the caller's own process group and succeeds. It now uses the same validationkill-daemonapplies before signalling. - 🐛
--toonwas ignored bylist-daemons, which fell through to the text table. - 🐛
kill-daemon --allprinted "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 schemewss:in thelist-daemonsendpoint column.
5. Impact & Risk Assessment
⚠️ Breaking Changes
kill-daemonis now scoped. Scripts relying on a barekill-daemonto stop all background processes now stop only the daemon for the default (or explicitly passed) profile. Usekill-daemon --allfor a global sweep.kill-daemoncan now fail where it previously succeeded. A scoped kill must resolve its target endpoint, so ifDevToolsActivePortis unreadable — the browser has already exited, or the profile is new — it exits non-zero instead of doing nothing. Cleanup traps running a barekill-daemonunderset -ewill 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 --allsweeps. Run it once after upgrading, or wait out the idle timeout. - Pass
--browsereven with--ws-endpoint/--user-data-dir. Neither needs it to connect, but--browseris 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. NoEDGE_*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-daemonremains unsupported: it reports that and exits without signalling or removing files. Take the PID fromlist-daemonsand usetaskkill /PID <pid>. - Enterprise-managed Edge can have remote debugging disabled by policy, in which case
DevToolsActivePortnever 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-pagesbefore assuming isolation.