← Files OneGateARCHIVED FILE
skills/onegate-dapp-debug/scripts/runtime/browser-session.mjs
27.7 KB · Oct 3, 2026 · 06:30 UTC
import { randomUUID } from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
import { constants as fsConstants } from "node:fs";
import {
access,
mkdtemp,
readFile,
rm,
stat,
} from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DapiEngine, DapiErrorCode } from "./dapi-engine.mjs";
import { defaultIdentityStore } from "./identity.mjs";
const DEFAULT_PROFILE_URL = new URL("../../assets/profiles/default.json", import.meta.url);
const MOCK_SOURCE_URL = new URL("../../assets/nep21-mock.js", import.meta.url);
const START_TIMEOUT_MS = 15_000;
const CDP_TIMEOUT_MS = 15_000;
const MAX_LOG_ENTRIES = 2_000;
const DAPI_BRIDGE_BINDING = "__OneGateMockBridge";
const DAPI_BRIDGE_RECEIVER = "__OneGateMockBridgeReceive";
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function removeSessionDirectory(sessionRoot) {
return rm(sessionRoot, {
recursive: true,
force: true,
maxRetries: 8,
retryDelay: 100,
});
}
function requireHttpUrl(value) {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("url must be a non-empty HTTP(S) URL.");
}
let parsed;
try {
parsed = new URL(value.trim());
} catch {
throw new Error("url must be a valid HTTP(S) URL.");
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error("url must use http:// or https://.");
}
if (parsed.username || parsed.password) {
throw new Error("url must not contain embedded credentials.");
}
return parsed.href;
}
async function isExecutableFile(candidate) {
try {
const info = await stat(candidate);
if (!info.isFile()) {
return false;
}
if (process.platform !== "win32") {
await access(candidate, fsConstants.X_OK);
}
return true;
} catch {
return false;
}
}
function resolveCommand(command) {
const resolver = process.platform === "win32" ? "where.exe" : "which";
const result = spawnSync(resolver, [command], {
encoding: "utf8",
windowsHide: true,
});
if (result.status !== 0) {
return undefined;
}
return result.stdout
.split(/\r?\n/u)
.map((line) => line.trim())
.find(Boolean);
}
function platformBrowserCandidates() {
if (process.platform === "win32") {
const localAppData = process.env.LOCALAPPDATA;
return [
["Chromium browser", resolveCommand("chromium")],
["Google Chrome", resolveCommand("chrome")],
["Microsoft Edge", resolveCommand("msedge")],
["Microsoft Edge", "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"],
["Microsoft Edge", "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"],
...(localAppData
? [["Microsoft Edge", path.join(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")]]
: []),
["Google Chrome", "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"],
["Google Chrome", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"],
...(localAppData
? [["Google Chrome", path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe")]]
: []),
];
}
if (process.platform === "darwin") {
const home = os.homedir();
return [
["Chromium browser", resolveCommand("chromium")],
["Google Chrome", resolveCommand("google-chrome")],
["Microsoft Edge", resolveCommand("microsoft-edge")],
["Microsoft Edge", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
["Microsoft Edge", path.join(home, "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")],
["Google Chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
["Google Chrome", path.join(home, "Applications/Google Chrome.app/Contents/MacOS/Google Chrome")],
["Chromium", "/Applications/Chromium.app/Contents/MacOS/Chromium"],
];
}
return [
["Microsoft Edge", resolveCommand("microsoft-edge")],
["Microsoft Edge", resolveCommand("microsoft-edge-stable")],
["Google Chrome", resolveCommand("google-chrome")],
["Google Chrome", resolveCommand("google-chrome-stable")],
["Chromium", resolveCommand("chromium")],
["Chromium", resolveCommand("chromium-browser")],
];
}
export async function discoverBrowsers() {
const configured = [
["ONEGATE_BROWSER_EXECUTABLE", process.env.ONEGATE_BROWSER_EXECUTABLE],
["CHROMIUM_PATH", process.env.CHROMIUM_PATH],
["CHROME_PATH", process.env.CHROME_PATH],
].filter(([, executable]) => executable);
const raw = [
...configured.map(([source, executable]) => ["Configured Chromium browser", path.resolve(executable), source]),
...platformBrowserCandidates(),
];
const found = [];
const seen = new Set();
for (const [name, executable, configuredSource] of raw) {
if (!executable) {
continue;
}
const normalized = path.resolve(executable);
const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
if (seen.has(key) || !(await isExecutableFile(normalized))) {
continue;
}
seen.add(key);
found.push({
name,
executable: normalized,
source: configuredSource ?? "auto",
});
}
return found;
}
async function resolveBrowserExecutable(requested) {
if (requested !== undefined) {
if (typeof requested !== "string" || requested.trim().length === 0) {
throw new Error("browserExecutable must be a non-empty absolute path.");
}
if (!path.isAbsolute(requested)) {
throw new Error("browserExecutable must be an absolute path.");
}
const normalized = path.resolve(requested);
if (!(await isExecutableFile(normalized))) {
throw new Error(`Chromium browser executable was not found: ${normalized}`);
}
return normalized;
}
const browsers = await discoverBrowsers();
if (browsers.length === 0) {
throw new Error(
"No CDP-compatible Chromium browser was found. Pass browserExecutable or set ONEGATE_BROWSER_EXECUTABLE, CHROMIUM_PATH, or CHROME_PATH.",
);
}
return browsers[0].executable;
}
async function loadProfile(profilePath) {
let source = DEFAULT_PROFILE_URL;
if (profilePath !== undefined) {
if (typeof profilePath !== "string" || profilePath.trim().length === 0) {
throw new Error("profilePath must be a non-empty absolute path.");
}
if (!path.isAbsolute(profilePath)) {
throw new Error("profilePath must be an absolute path.");
}
source = path.resolve(profilePath);
}
let profile;
try {
profile = JSON.parse(await readFile(source, "utf8"));
} catch (error) {
throw new Error(
`Unable to read Browser Mock profile: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (profile === null || typeof profile !== "object" || Array.isArray(profile)) {
throw new Error("Browser Mock profile must be a JSON object.");
}
return profile;
}
export function createInjectionSource(sessionId, profile, mockSource) {
const envelopeJson = JSON.stringify({ sessionId, profile });
const encodedEnvelope = JSON.stringify(envelopeJson)
.replaceAll("\u2028", "\\u2028")
.replaceAll("\u2029", "\\u2029");
return [
"if (globalThis.top === globalThis) {",
"Object.defineProperty(globalThis, \"__ONEGATE_MOCK_CONFIG__\", {",
" configurable: false,",
" enumerable: false,",
" writable: false,",
` value: JSON.parse(${encodedEnvelope})`,
"});",
mockSource,
"}",
"//# sourceURL=onegate://browser-mock/nep21-mock.js",
].join("\n");
}
class CdpClient {
constructor(socket) {
this.socket = socket;
this.nextId = 1;
this.pending = new Map();
this.waiters = new Map();
this.eventHandler = undefined;
this.closed = false;
socket.addEventListener("message", (event) => {
void this.handleMessage(event.data);
});
socket.addEventListener("close", () => this.handleClose());
socket.addEventListener("error", () => this.handleClose());
}
static async connect(webSocketUrl) {
const socket = new WebSocket(webSocketUrl);
await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("Timed out while connecting to the Chromium DevTools endpoint.")),
CDP_TIMEOUT_MS,
);
socket.addEventListener(
"open",
() => {
clearTimeout(timeout);
resolve();
},
{ once: true },
);
socket.addEventListener(
"error",
() => {
clearTimeout(timeout);
reject(new Error("Unable to connect to the Chromium DevTools endpoint."));
},
{ once: true },
);
});
return new CdpClient(socket);
}
setEventHandler(handler) {
this.eventHandler = handler;
}
async handleMessage(data) {
let text;
if (typeof data === "string") {
text = data;
} else if (data instanceof Blob) {
text = await data.text();
} else {
text = Buffer.from(data).toString("utf8");
}
let message;
try {
message = JSON.parse(text);
} catch {
return;
}
if (message.id !== undefined) {
const pending = this.pending.get(message.id);
if (pending === undefined) {
return;
}
this.pending.delete(message.id);
clearTimeout(pending.timeout);
if (message.error) {
pending.reject(new Error(`${pending.method} failed: ${message.error.message}`));
} else {
pending.resolve(message.result ?? {});
}
return;
}
const methodWaiters = this.waiters.get(message.method);
if (methodWaiters) {
for (const waiter of [...methodWaiters]) {
if (!waiter.predicate || waiter.predicate(message.params ?? {})) {
methodWaiters.delete(waiter);
clearTimeout(waiter.timeout);
waiter.resolve(message.params ?? {});
}
}
}
this.eventHandler?.(message.method, message.params ?? {});
}
handleClose() {
if (this.closed) {
return;
}
this.closed = true;
for (const pending of this.pending.values()) {
clearTimeout(pending.timeout);
pending.reject(new Error("Chromium DevTools connection closed."));
}
this.pending.clear();
for (const waiters of this.waiters.values()) {
for (const waiter of waiters) {
clearTimeout(waiter.timeout);
waiter.reject(new Error("Chromium DevTools connection closed."));
}
}
this.waiters.clear();
}
send(method, params = {}) {
if (this.closed) {
return Promise.reject(new Error("Chromium DevTools connection is closed."));
}
const id = this.nextId++;
const response = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`${method} timed out.`));
}, CDP_TIMEOUT_MS);
this.pending.set(id, { resolve, reject, timeout, method });
});
this.socket.send(JSON.stringify({ id, method, params }));
return response;
}
waitForEvent(method, predicate, timeoutMs = CDP_TIMEOUT_MS) {
if (this.closed) {
return Promise.reject(new Error("Chromium DevTools connection is closed."));
}
return new Promise((resolve, reject) => {
const waiters = this.waiters.get(method) ?? new Set();
const waiter = {
predicate,
resolve,
reject,
timeout: setTimeout(() => {
waiters.delete(waiter);
reject(new Error(`${method} event timed out.`));
}, timeoutMs),
};
waiters.add(waiter);
this.waiters.set(method, waiters);
});
}
close() {
if (!this.closed) {
this.socket.close();
}
this.handleClose();
}
}
async function waitForDevToolsPort(userDataDir, childProcess, getStderr) {
const portFile = path.join(userDataDir, "DevToolsActivePort");
const deadline = Date.now() + START_TIMEOUT_MS;
while (Date.now() < deadline) {
try {
const [portLine] = (await readFile(portFile, "utf8")).split(/\r?\n/u);
const port = Number.parseInt(portLine, 10);
if (Number.isInteger(port) && port > 0 && port <= 65_535) {
return port;
}
} catch {
// The file appears only after Chromium has opened its DevTools endpoint.
}
if (childProcess.exitCode !== null && childProcess.exitCode !== 0) {
throw new Error(
`Chromium exited before DevTools was ready.${getStderr() ? ` ${getStderr()}` : ""}`,
);
}
await delay(50);
}
throw new Error(
`Timed out while waiting for Chromium DevTools.${getStderr() ? ` ${getStderr()}` : ""}`,
);
}
async function findPageTarget(port) {
const deadline = Date.now() + START_TIMEOUT_MS;
while (Date.now() < deadline) {
try {
const response = await fetch(
`http://127.0.0.1:${port}/json/new?${encodeURIComponent("about:blank")}`,
{
method: "PUT",
signal: AbortSignal.timeout(2_000),
},
);
if (response.ok) {
const target = await response.json();
if (target?.type === "page" && target.webSocketDebuggerUrl) {
return target;
}
}
} catch {
// Chromium may report its port just before the HTTP endpoint is ready.
}
try {
const response = await fetch(`http://127.0.0.1:${port}/json/list`, {
signal: AbortSignal.timeout(2_000),
});
if (response.ok) {
const targets = await response.json();
const target = targets.find((candidate) => candidate.type === "page");
if (target?.webSocketDebuggerUrl) {
return target;
}
}
} catch {
// Chromium may report its port just before the HTTP endpoint is ready.
}
await delay(50);
}
throw new Error("Chromium did not create a debuggable page target.");
}
function formatRemoteObject(remoteObject) {
if (Object.prototype.hasOwnProperty.call(remoteObject, "value")) {
if (typeof remoteObject.value === "string") {
return remoteObject.value;
}
try {
return JSON.stringify(remoteObject.value);
} catch {
return String(remoteObject.value);
}
}
return remoteObject.unserializableValue ?? remoteObject.description ?? remoteObject.type;
}
function evaluationValue(response) {
if (response.exceptionDetails) {
const description =
response.exceptionDetails.exception?.description ??
response.exceptionDetails.text ??
"JavaScript evaluation failed.";
throw new Error(description);
}
const remoteObject = response.result ?? {};
if (Object.prototype.hasOwnProperty.call(remoteObject, "value")) {
return remoteObject.value;
}
if (remoteObject.unserializableValue !== undefined) {
return remoteObject.unserializableValue;
}
return {
type: remoteObject.type,
subtype: remoteObject.subtype,
description: remoteObject.description,
};
}
function killProcessTree(childProcess) {
if (!childProcess || childProcess.exitCode !== null) {
return;
}
if (process.platform === "win32") {
spawnSync("taskkill.exe", ["/PID", String(childProcess.pid), "/T", "/F"], {
stdio: "ignore",
windowsHide: true,
});
return;
}
childProcess.kill("SIGTERM");
}
export class BrowserMockSession {
static async start(options) {
const url = requireHttpUrl(options?.url);
const browserExecutable = await resolveBrowserExecutable(options?.browserExecutable);
const profile = await loadProfile(options?.profilePath);
const identity = options?.identity ?? await defaultIdentityStore.load();
const engine = new DapiEngine({ identity, profile });
const sessionId = randomUUID();
const sessionRoot = await mkdtemp(path.join(os.tmpdir(), "onegate-browser-mock-"));
const userDataDir = path.join(sessionRoot, "profile");
const headless = options?.headless === true;
const preventEdgeCompatibilityRelaunch = process.platform === "win32"
&& path.basename(browserExecutable).toLowerCase() === "msedge.exe";
const args = [
...(preventEdgeCompatibilityRelaunch ? ["--edge-skip-compat-layer-relaunch"] : []),
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${userDataDir}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-default-apps",
"--disable-extensions",
"--window-size=1440,1000",
...(headless ? ["--headless=new", "--hide-scrollbars"] : []),
"about:blank",
];
let childProcess;
let cdp;
let session;
let stderr = "";
try {
childProcess = spawn(browserExecutable, args, {
stdio: ["ignore", "ignore", "pipe"],
windowsHide: true,
});
childProcess.stderr.setEncoding("utf8");
childProcess.stderr.on("data", (chunk) => {
stderr = `${stderr}${chunk}`.slice(-16_384).trim();
});
const port = await waitForDevToolsPort(userDataDir, childProcess, () => stderr);
const pageTarget = await findPageTarget(port);
cdp = await CdpClient.connect(pageTarget.webSocketDebuggerUrl);
session = new BrowserMockSession({
sessionId,
url,
browserExecutable,
headless,
profile,
identity,
engine,
sessionRoot,
userDataDir,
childProcess,
cdp,
});
await session.initialize();
return session;
} catch (error) {
const diagnostic = [
session?.initializationStage
? `stage=${session.initializationStage}`
: undefined,
childProcess?.exitCode !== null && childProcess?.exitCode !== undefined
? `exitCode=${childProcess.exitCode}`
: undefined,
stderr ? `stderr=${stderr}` : undefined,
]
.filter(Boolean)
.join("; ");
cdp?.close();
killProcessTree(childProcess);
await removeSessionDirectory(sessionRoot);
throw new Error(
`${error instanceof Error ? error.message : String(error)}${diagnostic ? ` (${diagnostic})` : ""}`,
{ cause: error },
);
}
}
constructor(options) {
Object.assign(this, options);
this.createdAt = new Date().toISOString();
this.logs = [];
this.nextLogSequence = 1;
this.stopped = false;
this.injectionIdentifier = undefined;
this.cdp.setEventHandler((method, params) => this.handleEvent(method, params));
}
appendLog(entry) {
this.logs.push({
sequence: this.nextLogSequence++,
timestamp: entry.timestamp ?? new Date().toISOString(),
...entry,
});
if (this.logs.length > MAX_LOG_ENTRIES) {
this.logs.shift();
}
}
handleEvent(method, params) {
if (method === "Runtime.bindingCalled" && params.name === DAPI_BRIDGE_BINDING) {
void this.handleBridgeRequest(params).catch((error) => {
this.appendLog({
source: "bridge",
level: "error",
message: error instanceof Error ? error.message : String(error),
});
});
return;
}
if (method === "Runtime.consoleAPICalled") {
const args = (params.args ?? []).map((argument) => ({
type: argument.type,
value: formatRemoteObject(argument),
}));
this.appendLog({
source: "console",
level: params.type ?? "log",
message: args.map((argument) => argument.value).join(" "),
args,
timestamp: params.timestamp
? new Date(params.timestamp).toISOString()
: undefined,
});
return;
}
if (method === "Runtime.exceptionThrown") {
this.appendLog({
source: "runtime",
level: "error",
message:
params.exceptionDetails?.exception?.description ??
params.exceptionDetails?.text ??
"Uncaught exception",
});
return;
}
if (method === "Log.entryAdded") {
this.appendLog({
source: params.entry?.source ?? "browser",
level: params.entry?.level ?? "info",
message: params.entry?.text ?? "",
url: params.entry?.url,
lineNumber: params.entry?.lineNumber,
timestamp: params.entry?.timestamp
? new Date(params.entry.timestamp).toISOString()
: undefined,
});
}
}
async handleBridgeRequest(params) {
let request;
try {
request = JSON.parse(params.payload);
} catch {
throw new Error("The DApp sent an invalid OneGate bridge payload.");
}
if (
request?.sessionId !== this.sessionId ||
typeof request?.id !== "string" ||
typeof request?.method !== "string" ||
!Array.isArray(request?.args)
) {
throw new Error("The DApp sent an invalid OneGate bridge request.");
}
let response;
try {
const contextResponse = await this.cdp.send("Runtime.evaluate", {
contextId: params.executionContextId,
expression: "({ host: location.hostname, topLevel: globalThis.top === globalThis })",
returnByValue: true,
});
const pageContext = evaluationValue(contextResponse);
if (pageContext?.topLevel !== true) {
throw Object.assign(new Error("OneGate Browser Mock is available only in the top-level DApp document."), {
code: DapiErrorCode.INVALID,
});
}
const result = await this.engine.invoke(request.method, request.args, {
host: pageContext.host,
});
response = { id: request.id, ok: true, result };
} catch (error) {
response = {
id: request.id,
ok: false,
error: {
code: Number.isInteger(error?.code) ? error.code : DapiErrorCode.UNKNOWN,
message: error instanceof Error ? error.message : String(error),
...(error?.data !== undefined ? { data: error.data } : {}),
},
};
}
const encodedResponse = JSON.stringify(JSON.stringify(response));
const delivered = await this.cdp.send("Runtime.evaluate", {
contextId: params.executionContextId,
expression: `globalThis[${JSON.stringify(DAPI_BRIDGE_RECEIVER)}](${encodedResponse})`,
returnByValue: true,
});
evaluationValue(delivered);
}
ensureRunning() {
if (this.stopped || this.childProcess.exitCode !== null) {
throw new Error(`Browser Mock session ${this.sessionId} is not running.`);
}
}
async initialize() {
this.initializationStage = "enable-domains";
await Promise.all([
this.cdp.send("Page.enable"),
this.cdp.send("Runtime.enable"),
this.cdp.send("Log.enable"),
]);
this.initializationStage = "register-private-dapi-bridge";
await this.cdp.send("Runtime.addBinding", { name: DAPI_BRIDGE_BINDING });
const [mockSource] = await Promise.all([readFile(MOCK_SOURCE_URL, "utf8")]);
const publicProfile = this.engine.publicConfiguration(this.sessionId).profile;
const injection = createInjectionSource(this.sessionId, publicProfile, mockSource);
this.initializationStage = "register-document-start-script";
const registered = await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
source: injection,
});
this.injectionIdentifier = registered.identifier;
this.initializationStage = "navigate";
const navigation = await this.cdp.send("Page.navigate", { url: this.url });
if (navigation.errorText) {
throw new Error(`Navigation failed: ${navigation.errorText}`);
}
this.initializationStage = "wait-for-provider";
await this.waitUntilReady();
this.initializationStage = "ready";
}
async waitUntilReady() {
const deadline = Date.now() + START_TIMEOUT_MS;
let lastError;
while (Date.now() < deadline) {
try {
const snapshot = await this.snapshot();
if (
snapshot.mock === true &&
(snapshot.readyState === "interactive" || snapshot.readyState === "complete")
) {
return snapshot;
}
} catch (error) {
lastError = error;
}
await delay(50);
}
throw new Error(
`OneGate provider was not ready after navigation.${lastError ? ` ${lastError.message}` : ""}`,
);
}
async evaluate(expression) {
this.ensureRunning();
if (typeof expression !== "string" || expression.trim().length === 0) {
throw new Error("expression must be a non-empty JavaScript expression.");
}
if (expression.length > 100_000) {
throw new Error("expression must not exceed 100,000 characters.");
}
const response = await this.cdp.send("Runtime.evaluate", {
expression,
awaitPromise: true,
returnByValue: true,
userGesture: true,
});
return evaluationValue(response);
}
async snapshot() {
return this.evaluate(`(() => {
const provider = window.OneGateDapiProvider;
return {
href: location.href,
origin: location.origin,
title: document.title,
readyState: document.readyState,
mock: Boolean(window.__OneGateMock && provider && provider.extra && provider.extra.mock),
provider: provider ? {
name: provider.name,
version: provider.version,
dapiVersion: provider.dapiVersion,
network: provider.network,
supportedNetworks: provider.supportedNetworks,
compatibility: provider.compatibility,
extra: provider.extra
} : null
};
})()`);
}
async status() {
const snapshot = await this.snapshot();
return {
sessionId: this.sessionId,
createdAt: this.createdAt,
requestedUrl: this.url,
browserExecutable: this.browserExecutable,
headless: this.headless,
injectionIdentifier: this.injectionIdentifier,
transactionMode: this.engine.transactionMode,
identity: this.identity.publicSummary(),
...snapshot,
};
}
getLogs(afterSequence = 0) {
this.ensureRunning();
if (!Number.isInteger(afterSequence) || afterSequence < 0) {
throw new Error("afterSequence must be a non-negative integer.");
}
return this.logs.filter((entry) => entry.sequence > afterSequence);
}
async getTrace() {
return this.evaluate("window.__OneGateMock ? window.__OneGateMock.getTrace() : []");
}
async screenshot() {
this.ensureRunning();
const result = await this.cdp.send("Page.captureScreenshot", {
format: "png",
fromSurface: true,
captureBeyondViewport: false,
});
return result.data;
}
async reload(ignoreCache = false) {
this.ensureRunning();
const contextCleared = this.cdp
.waitForEvent("Runtime.executionContextsCleared", undefined, 2_000)
.catch(() => undefined);
await this.cdp.send("Page.reload", { ignoreCache: ignoreCache === true });
await contextCleared;
return this.waitUntilReady();
}
async stop() {
if (this.stopped) {
return;
}
this.stopped = true;
try {
await this.cdp.send("Browser.close");
} catch {
// Closing the browser usually closes the CDP socket before the reply arrives.
}
this.cdp.close();
const deadline = Date.now() + 2_000;
while (this.childProcess.exitCode === null && Date.now() < deadline) {
await delay(50);
}
killProcessTree(this.childProcess);
await removeSessionDirectory(this.sessionRoot);
}
}
SHA-256: 1ad2ee925341023a75d60f7b5f9b66597e4b6a96594cebba7b96f2c0a55c7ceb