mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-15 06:59:48 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93e54a7b86 | |||
| c1f8a9ad07 | |||
| 8b8ecf61a6 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.69.0"
|
"version": "1.69.1"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.69.1 - 2026-03-16
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-chrome-cdp`: tighten chrome auto-connect logic to reduce false positives
|
||||||
|
|
||||||
## 1.69.0 - 2026-03-16
|
## 1.69.0 - 2026-03-16
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.69.1 - 2026-03-16
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-chrome-cdp`:收紧 Chrome 自动连接逻辑,减少误连接
|
||||||
|
|
||||||
## 1.69.0 - 2026-03-16
|
## 1.69.0 - 2026-03-16
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.69.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.69.1**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ async function fetch_cookies_from_existing_chrome(
|
|||||||
if (verbose) logger.info(`Found existing Chrome on port ${discovered.port}. Connecting via WebSocket...`);
|
if (verbose) logger.info(`Found existing Chrome on port ${discovered.port}. Connecting via WebSocket...`);
|
||||||
|
|
||||||
let cdp: CdpConnection | null = null;
|
let cdp: CdpConnection | null = null;
|
||||||
let createdTab = false;
|
|
||||||
let targetId: string | null = null;
|
let targetId: string | null = null;
|
||||||
|
let createdTarget = false;
|
||||||
try {
|
try {
|
||||||
const connectStart = Date.now();
|
const connectStart = Date.now();
|
||||||
const connectTimeout = 30_000;
|
const connectTimeout = 30_000;
|
||||||
@@ -129,14 +129,6 @@ async function fetch_cookies_from_existing_chrome(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets');
|
|
||||||
const hasGeminiTab = targets.targetInfos.some(
|
|
||||||
(t) => t.type === 'page' && t.url.includes('gemini.google.com'),
|
|
||||||
);
|
|
||||||
createdTab = !hasGeminiTab;
|
|
||||||
|
|
||||||
if (verbose) logger.debug(hasGeminiTab ? 'Found existing Gemini tab, attaching...' : 'No Gemini tab found, creating new tab...');
|
|
||||||
|
|
||||||
const page = await openPageSession({
|
const page = await openPageSession({
|
||||||
cdp,
|
cdp,
|
||||||
reusing: false,
|
reusing: false,
|
||||||
@@ -147,6 +139,9 @@ async function fetch_cookies_from_existing_chrome(
|
|||||||
});
|
});
|
||||||
const { sessionId } = page;
|
const { sessionId } = page;
|
||||||
targetId = page.targetId;
|
targetId = page.targetId;
|
||||||
|
createdTarget = page.createdTarget;
|
||||||
|
|
||||||
|
if (verbose) logger.debug(createdTarget ? 'No Gemini tab found, creating new tab...' : 'Found existing Gemini tab, attaching...');
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let last: CookieMap = {};
|
let last: CookieMap = {};
|
||||||
@@ -176,7 +171,7 @@ async function fetch_cookies_from_existing_chrome(
|
|||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (cdp) {
|
if (cdp) {
|
||||||
if (createdTab && targetId) {
|
if (createdTarget && targetId) {
|
||||||
try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {}
|
try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {}
|
||||||
}
|
}
|
||||||
cdp.close();
|
cdp.close();
|
||||||
|
|||||||
+136
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
+136
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
+136
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
+136
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
+136
@@ -1,4 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -7,9 +8,11 @@ import process from "node:process";
|
|||||||
import test, { type TestContext } from "node:test";
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
discoverRunningChromeDebugPort,
|
||||||
findChromeExecutable,
|
findChromeExecutable,
|
||||||
findExistingChromeDebugPort,
|
findExistingChromeDebugPort,
|
||||||
getFreePort,
|
getFreePort,
|
||||||
|
openPageSession,
|
||||||
resolveSharedChromeProfileDir,
|
resolveSharedChromeProfileDir,
|
||||||
waitForChromeDebugPort,
|
waitForChromeDebugPort,
|
||||||
} from "./index.ts";
|
} from "./index.ts";
|
||||||
@@ -74,6 +77,39 @@ async function closeServer(server: http.Server): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPathForPlatform(): string | null {
|
||||||
|
if (process.platform === "win32") return null;
|
||||||
|
return "/bin/bash";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
|
||||||
|
const shell = shellPathForPlatform();
|
||||||
|
if (!shell) return null;
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
shell,
|
||||||
|
[
|
||||||
|
"-lc",
|
||||||
|
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProcess(child: ChildProcess | null): Promise<void> {
|
||||||
|
if (!child) return;
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||||
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||||
|
await new Promise((resolve) => child.once("exit", resolve));
|
||||||
|
}
|
||||||
|
|
||||||
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
|
||||||
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
useEnv(t, { TEST_FIXED_PORT: "45678" });
|
||||||
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
|
||||||
@@ -153,6 +189,106 @@ test("findExistingChromeDebugPort reads DevToolsActivePort and validates it agai
|
|||||||
assert.equal(found, port);
|
assert.equal(found, port);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.deepEqual(found, {
|
||||||
|
port,
|
||||||
|
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
t.skip("Process discovery fallback is not used on Windows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = await makeTempDir("baoyu-cdp-user-data-");
|
||||||
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startDebugServer(port);
|
||||||
|
t.after(() => closeServer(server));
|
||||||
|
|
||||||
|
const fakeChromium = await startFakeChromiumProcess(port);
|
||||||
|
t.after(async () => { await stopProcess(fakeChromium); });
|
||||||
|
|
||||||
|
const found = await discoverRunningChromeDebugPort({
|
||||||
|
userDataDirs: [root],
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
assert.equal(found, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openPageSession reports whether it created a new target", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const cdpExisting = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
calls.push(method);
|
||||||
|
if (method === "Target.getTargets") {
|
||||||
|
return {
|
||||||
|
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = await openPageSession({
|
||||||
|
cdp: cdpExisting as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(existing, {
|
||||||
|
sessionId: "session-existing",
|
||||||
|
targetId: "existing-target",
|
||||||
|
createdTarget: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
|
||||||
|
|
||||||
|
const createCalls: string[] = [];
|
||||||
|
const cdpCreated = {
|
||||||
|
send: async <T>(method: string): Promise<T> => {
|
||||||
|
createCalls.push(method);
|
||||||
|
if (method === "Target.getTargets") return { targetInfos: [] } as T;
|
||||||
|
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
|
||||||
|
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
|
||||||
|
throw new Error(`Unexpected method: ${method}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await openPageSession({
|
||||||
|
cdp: cdpCreated as never,
|
||||||
|
reusing: false,
|
||||||
|
url: "https://gemini.google.com/app",
|
||||||
|
matchTarget: (target) => target.url.includes("gemini.google.com"),
|
||||||
|
activateTarget: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(created, {
|
||||||
|
sessionId: "session-created",
|
||||||
|
targetId: "created-target",
|
||||||
|
createdTarget: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export type DiscoveredChrome = {
|
|||||||
|
|
||||||
type DiscoverRunningChromeOptions = {
|
type DiscoverRunningChromeOptions = {
|
||||||
channels?: ChromeChannel[];
|
channels?: ChromeChannel[];
|
||||||
|
userDataDirs?: string[];
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ type OpenPageSessionOptions = {
|
|||||||
export type PageSession = {
|
export type PageSession = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
createdTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
@@ -273,7 +275,8 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||||
const timeoutMs = options.timeoutMs ?? 3_000;
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
||||||
|
|
||||||
const userDataDirs = getDefaultChromeUserDataDirs(channels);
|
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
|
||||||
|
.map((dir) => path.resolve(dir));
|
||||||
for (const dir of userDataDirs) {
|
for (const dir of userDataDirs) {
|
||||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -288,7 +291,10 @@ export async function discoverRunningChromeDebugPort(options: DiscoverRunningChr
|
|||||||
if (result.status === 0 && result.stdout) {
|
if (result.status === 0 && result.stdout) {
|
||||||
const lines = result.stdout
|
const lines = result.stdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes("--remote-debugging-port=") && /chrome|chromium/i.test(line));
|
.filter((line) =>
|
||||||
|
line.includes("--remote-debugging-port=") &&
|
||||||
|
userDataDirs.some((dir) => line.includes(dir))
|
||||||
|
);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||||
@@ -479,10 +485,12 @@ export function killChrome(chrome: ChildProcess): void {
|
|||||||
|
|
||||||
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
|
||||||
let targetId: string;
|
let targetId: string;
|
||||||
|
let createdTarget = false;
|
||||||
|
|
||||||
if (options.reusing) {
|
if (options.reusing) {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
} else {
|
} else {
|
||||||
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
|
||||||
const existing = targets.targetInfos.find(options.matchTarget);
|
const existing = targets.targetInfos.find(options.matchTarget);
|
||||||
@@ -491,6 +499,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
} else {
|
} else {
|
||||||
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
|
||||||
targetId = created.targetId;
|
targetId = created.targetId;
|
||||||
|
createdTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,5 +516,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
|
|||||||
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||||
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
|
||||||
|
|
||||||
return { sessionId, targetId };
|
return { sessionId, targetId, createdTarget };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user