aboutsummaryrefslogtreecommitdiff
path: root/src/cdp
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-07-28 21:46:00 +0100
committerIgor Soarez <igor@soarez.org>2026-10-14 12:17:36 +0100
commit233f29aedf004699a97dbd9aee087e239f4a9e47 (patch)
tree9b7dca7e4943f86905764052d671c0ef5b348fca /src/cdp
parent384688bf85ba5c806a40a059983f43ca537385cb (diff)
feat(cdp): BU_CDP_HTTP to attach to a browser on another host
Upstream can only drive a browser on localhost. The pi extension always goes through the daemon (src/index.ts uses createDaemonTransport), the daemon resolves Chrome with discoverWsUrl() (src/daemon/bridge.ts), and that probe is hardcoded to 127.0.0.1 (src/cdp/discovery.ts). BU_CDP_WS is read only by the in-process client (src/client.ts), which that path never touches, so it has no effect on the extension - the only workaround is relaying the remote browser onto localhost first. BU_CDP_HTTP="host:port" points at the remote CDP HTTP endpoint. It is resolved inside discoverEndpoint(), so the daemon and the client both honour it. Deliberately an HTTP endpoint rather than a WebSocket URL: it is re-resolved on every discovery call, so the browser UUID is re-read after the remote browser restarts. Chrome re-mints that UUID on every launch, so a pinned WebSocket URL goes stale as soon as the remote browser is recycled. Also skips performSetup's local-process precondition when BU_CDP_HTTP is set: isBrowserRunning() enumerates local processes and can never see a remote browser. Step 4 still fails loudly if it is unreachable. No userDataDir is returned for remote endpoints, since the profile registry lives on the remote host - profile enumeration degrades to the pre-profile behaviour rather than reading the wrong machine's Local State.
Diffstat (limited to 'src/cdp')
-rw-r--r--src/cdp/discovery.ts33
1 files changed, 31 insertions, 2 deletions
diff --git a/src/cdp/discovery.ts b/src/cdp/discovery.ts
index 3de6e97..41bcfe5 100644
--- a/src/cdp/discovery.ts
+++ b/src/cdp/discovery.ts
@@ -63,10 +63,10 @@ const waitForPort = async (port: number): Promise<Result<void, CdpError>> => {
const isPortLive = async (port: number): Promise<boolean> => (await probePort(port)).success;
// /json/version is authoritative: a stale DevToolsActivePort file may name a browser that has since exited while another now owns the port.
-const queryLiveWsUrl = (port: number, timeoutMs = 1_500): Promise<string | null> =>
+const queryLiveWsUrl = (port: number, timeoutMs = 1_500, host = "127.0.0.1"): Promise<string | null> =>
new Promise<string | null>((resolve) => {
const req = httpRequest(
- { host: "127.0.0.1", port, path: "/json/version", method: "GET", timeout: timeoutMs },
+ { host, port, path: "/json/version", method: "GET", timeout: timeoutMs },
(res) => {
if (res.statusCode !== 200) {
res.resume();
@@ -114,6 +114,35 @@ export const discoverEndpoint = async (): Promise<Result<CdpEndpoint, CdpError>>
const override = endpointFromEnv();
if (override) return ok(override);
+ // LOCAL PATCH: direct remote browser support.
+ //
+ // BU_CDP_HTTP="host:port" points at a CDP *HTTP* endpoint on another machine.
+ // We ask it for its canonical webSocketDebuggerUrl on every discovery call,
+ // so the browser UUID is re-read after the remote Chrome restarts. That is
+ // the difference from the stock BU_CDP_WS (checked just above, so an explicit
+ // WebSocket URL still wins), which pins a UUID that Chrome re-mints on every
+ // launch.
+ //
+ // Returns no userDataDir: the profile registry lives on the remote host, so
+ // profile enumeration correctly degrades to the pre-profile behaviour.
+ const remote = process.env["BU_CDP_HTTP"]?.trim();
+ if (remote) {
+ const sep = remote.lastIndexOf(":");
+ const host = sep === -1 ? remote : remote.slice(0, sep);
+ const port = sep === -1 ? 9222 : Number(remote.slice(sep + 1));
+ if (!host || !Number.isInteger(port) || port <= 0 || port >= 65536) {
+ return err(cdpError("discovery_failed", `BU_CDP_HTTP is not a valid host:port — got "${remote}"`));
+ }
+ const ws = await queryLiveWsUrl(port, 5_000, host);
+ if (!ws) {
+ return err(cdpError(
+ "discovery_failed",
+ `No CDP endpoint answered at http://${host}:${port}/json/version (BU_CDP_HTTP). Is the remote browser running and reachable?`,
+ ));
+ }
+ return ok({ wsUrl: ws });
+ }
+
const dirs = userDataDirCandidates();
const candidates: Candidate[] = [];