ask-pro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +30 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/assets/ask-pro_logo.png +0 -0
- package/dist/bin/ask-pro-cli.js +507 -0
- package/dist/scripts/run-cli.js +27 -0
- package/dist/src/ask-pro/atomicWrite.js +26 -0
- package/dist/src/ask-pro/browserRunner.js +796 -0
- package/dist/src/ask-pro/responseZip.js +349 -0
- package/dist/src/ask-pro/session.js +662 -0
- package/dist/src/ask-pro/sessionControllerLease.js +64 -0
- package/dist/src/ask-pro/toon.js +26 -0
- package/dist/src/ask-pro/zip.js +85 -0
- package/dist/src/browser/actions/assistantResponse.js +1245 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
- package/dist/src/browser/actions/attachments.js +1720 -0
- package/dist/src/browser/actions/composerSendReadiness.js +369 -0
- package/dist/src/browser/actions/domEvents.js +31 -0
- package/dist/src/browser/actions/inputGuard.js +52 -0
- package/dist/src/browser/actions/modelPickerDom.js +68 -0
- package/dist/src/browser/actions/modelSelection.js +576 -0
- package/dist/src/browser/actions/navigation.js +510 -0
- package/dist/src/browser/actions/promptComposer.js +824 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingStatus.js +408 -0
- package/dist/src/browser/actions/thinkingTime.js +635 -0
- package/dist/src/browser/actions/windowState.js +47 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptModelCatalog.js +321 -0
- package/dist/src/browser/chromeLifecycle.js +807 -0
- package/dist/src/browser/config.js +110 -0
- package/dist/src/browser/constants.js +85 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +337 -0
- package/dist/src/browser/domDebug.js +72 -0
- package/dist/src/browser/errors.js +20 -0
- package/dist/src/browser/format.js +16 -0
- package/dist/src/browser/index.js +2631 -0
- package/dist/src/browser/language.js +97 -0
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/profilePaths.js +282 -0
- package/dist/src/browser/profileState.js +413 -0
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
- package/dist/src/browser/reattach.js +534 -0
- package/dist/src/browser/reattachHelpers.js +387 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/version.js +39 -0
- package/package.json +114 -0
- package/scripts/refresh-local-plugin.mjs +179 -0
- package/scripts/refresh-local-plugin.ps1 +93 -0
- package/skills/ask-pro/SKILL.md +181 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import CDP from "chrome-remote-interface";
|
|
10
|
+
import { Launcher } from "chrome-launcher";
|
|
11
|
+
import { clearChromeLaunchStarting, cleanupStaleProfileState, hasChromeLaunchStarting, markChromeLaunchStarting, readChromePid, readDevToolsPort, verifyDevToolsReachable, } from "./profileState.js";
|
|
12
|
+
import { delay } from "./utils.js";
|
|
13
|
+
import { formatElapsed } from "./format.js";
|
|
14
|
+
import { withTimeout } from "./reattachHelpers.js";
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
const CPU_THROTTLING_OVERRIDE_FLAGS = new Set([
|
|
17
|
+
"--disable-backgrounding-occluded-windows",
|
|
18
|
+
"--disable-renderer-backgrounding",
|
|
19
|
+
"--disable-background-timer-throttling",
|
|
20
|
+
"--disable-ipc-flooding-protection",
|
|
21
|
+
]);
|
|
22
|
+
const CHROME_STARTUP_HANDOFF_MS = 26_000;
|
|
23
|
+
export async function launchChrome(config, userDataDir, logger) {
|
|
24
|
+
const connectHost = resolveRemoteDebugHost();
|
|
25
|
+
const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
|
|
26
|
+
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
27
|
+
const chromeFlags = buildChromeLaunchFlags(buildChromeFlags(config.headless ?? false, debugBindAddress, config.acceptLanguage, {
|
|
28
|
+
startMinimized: shouldLaunchChromeMinimized(config),
|
|
29
|
+
}));
|
|
30
|
+
await markChromeLaunchStarting(userDataDir);
|
|
31
|
+
try {
|
|
32
|
+
const launcher = await launchManagedChrome({
|
|
33
|
+
chromeFlags,
|
|
34
|
+
chromePath: config.chromePath ?? undefined,
|
|
35
|
+
userDataDir,
|
|
36
|
+
host: connectHost,
|
|
37
|
+
requestedPort: debugPort ?? undefined,
|
|
38
|
+
});
|
|
39
|
+
const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
|
|
40
|
+
const hostLabel = connectHost ? ` on ${connectHost}` : "";
|
|
41
|
+
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
42
|
+
return Object.assign(launcher, { host: connectHost ?? "127.0.0.1" });
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await clearChromeLaunchStarting(userDataDir);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function maybeReuseRunningChrome(userDataDir, logger) {
|
|
49
|
+
let port = await readDevToolsPort(userDataDir);
|
|
50
|
+
if (!port) {
|
|
51
|
+
const startupHandoff = await hasChromeLaunchStarting(userDataDir);
|
|
52
|
+
if (startupHandoff) {
|
|
53
|
+
const deadline = Date.now() + CHROME_STARTUP_HANDOFF_MS;
|
|
54
|
+
logger(`Waiting up to ${formatElapsed(CHROME_STARTUP_HANDOFF_MS)} for brokered Chrome startup handoff...`);
|
|
55
|
+
while (!port && Date.now() < deadline) {
|
|
56
|
+
await delay(100);
|
|
57
|
+
port = await readDevToolsPort(userDataDir);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (startupHandoff)
|
|
61
|
+
await clearChromeLaunchStarting(userDataDir);
|
|
62
|
+
}
|
|
63
|
+
if (!port)
|
|
64
|
+
return null;
|
|
65
|
+
await clearChromeLaunchStarting(userDataDir);
|
|
66
|
+
const probe = await verifyDevToolsReachable({ port });
|
|
67
|
+
if (!probe.ok) {
|
|
68
|
+
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${probe.error}); launching new Chrome.`);
|
|
69
|
+
await cleanupStaleProfileState(userDataDir, logger, {
|
|
70
|
+
lockRemovalMode: "if_ask_pro_pid_dead",
|
|
71
|
+
});
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const pid = await readChromePid(userDataDir);
|
|
75
|
+
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ""})`);
|
|
76
|
+
return {
|
|
77
|
+
port,
|
|
78
|
+
pid: pid ?? undefined,
|
|
79
|
+
kill: async () => { },
|
|
80
|
+
process: undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function releaseChromeProcessHandle(chrome) {
|
|
84
|
+
const child = chrome?.process;
|
|
85
|
+
child?.stdin?.unref?.();
|
|
86
|
+
child?.stdout?.unref?.();
|
|
87
|
+
child?.stderr?.unref?.();
|
|
88
|
+
for (const stream of child?.stdio ?? [])
|
|
89
|
+
stream?.unref?.();
|
|
90
|
+
child?.unref?.();
|
|
91
|
+
}
|
|
92
|
+
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
|
|
93
|
+
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
94
|
+
let handling;
|
|
95
|
+
const handleSignal = (signal) => {
|
|
96
|
+
if (handling) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
handling = true;
|
|
100
|
+
const inFlight = opts?.isInFlight?.() ?? false;
|
|
101
|
+
const leaveRunning = keepBrowser || inFlight;
|
|
102
|
+
if (leaveRunning) {
|
|
103
|
+
logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger(`Received ${signal}; terminating Chrome process`);
|
|
107
|
+
}
|
|
108
|
+
void (async () => {
|
|
109
|
+
if (leaveRunning) {
|
|
110
|
+
// Ensure reattach hints are written before we exit.
|
|
111
|
+
await opts?.emitRuntimeHint?.().catch(() => undefined);
|
|
112
|
+
if (inFlight) {
|
|
113
|
+
logger('Session still in flight; reattach with "ask-pro --resume <session-id>" to continue.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
try {
|
|
118
|
+
await closeChromeGracefully(chrome, logger);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// ignore close failures
|
|
122
|
+
}
|
|
123
|
+
if (opts?.preserveUserDataDir) {
|
|
124
|
+
// Preserve the profile directory (manual login), but clear reattach hints so we don't
|
|
125
|
+
// try to reuse a dead DevTools port on the next run.
|
|
126
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})().finally(() => {
|
|
133
|
+
const exitCode = signal === "SIGINT" ? 130 : 1;
|
|
134
|
+
// Vitest treats any `process.exit()` call as an unhandled failure, even if mocked.
|
|
135
|
+
// Keep production behavior (hard-exit on signals) while letting tests observe state changes.
|
|
136
|
+
process.exitCode = exitCode;
|
|
137
|
+
const isTestRun = process.env.VITEST === "1" || process.env.NODE_ENV === "test";
|
|
138
|
+
if (!isTestRun) {
|
|
139
|
+
process.exit(exitCode);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
for (const signal of signals) {
|
|
144
|
+
process.on(signal, handleSignal);
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
for (const signal of signals) {
|
|
148
|
+
process.removeListener(signal, handleSignal);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export async function closeChromeGracefully(chrome, logger) {
|
|
153
|
+
const port = chrome.port;
|
|
154
|
+
const host = chrome.host ?? "127.0.0.1";
|
|
155
|
+
const shutdownDeadline = Date.now() + 20_000;
|
|
156
|
+
try {
|
|
157
|
+
await withTimeout((async () => {
|
|
158
|
+
if (!port)
|
|
159
|
+
throw new Error("missing DevTools port");
|
|
160
|
+
const version = (await CDP.Version({ host, port }));
|
|
161
|
+
const target = version.webSocketDebuggerUrl;
|
|
162
|
+
const client = (await CDP(target ? { target, local: true } : { host, port }));
|
|
163
|
+
try {
|
|
164
|
+
await client.Browser?.close?.();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await client.close().catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
await waitForChromeExit(chrome.pid, Math.max(0, shutdownDeadline - Date.now()));
|
|
170
|
+
if (chrome.pid && isPidAlive(chrome.pid)) {
|
|
171
|
+
throw new Error(`Chrome process ${chrome.pid} remained alive after Browser.close`);
|
|
172
|
+
}
|
|
173
|
+
})(), 20_000, "Chrome did not exit within 20 seconds after Browser.close");
|
|
174
|
+
chrome.process?.unref?.();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (logger?.verbose) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
logger(`Graceful Chrome close failed (${message}); terminating process.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await chrome.kill();
|
|
184
|
+
await waitForChromeExit(chrome.pid, 2500);
|
|
185
|
+
if (chrome.pid && isPidAlive(chrome.pid)) {
|
|
186
|
+
throw new Error(`Chrome process ${chrome.pid} remained alive after termination.`);
|
|
187
|
+
}
|
|
188
|
+
chrome.process?.unref?.();
|
|
189
|
+
}
|
|
190
|
+
async function waitForChromeExit(pid, timeoutMs) {
|
|
191
|
+
if (!pid) {
|
|
192
|
+
await delay(500);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const deadline = Date.now() + timeoutMs;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (!isPidAlive(pid))
|
|
198
|
+
return;
|
|
199
|
+
await delay(100);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function isPidAlive(pid) {
|
|
203
|
+
try {
|
|
204
|
+
process.kill(pid, 0);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export async function hideChromeWindow(chrome, logger, deps = {}) {
|
|
212
|
+
const platform = deps.platform ?? process.platform;
|
|
213
|
+
const runExecFile = deps.execFileAsync ?? execFileAsync;
|
|
214
|
+
if (!chrome.pid) {
|
|
215
|
+
logger("Unable to hide window: missing Chrome PID");
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (platform === "win32") {
|
|
219
|
+
return setWindowsChromeWindowVisibility(chrome.pid, 0, "hidden", logger, runExecFile);
|
|
220
|
+
}
|
|
221
|
+
if (platform !== "darwin") {
|
|
222
|
+
logger("Window hiding is only supported on macOS and Windows");
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const script = `tell application "System Events"
|
|
226
|
+
try
|
|
227
|
+
set visible of (first process whose unix id is ${chrome.pid}) to false
|
|
228
|
+
end try
|
|
229
|
+
end tell`;
|
|
230
|
+
try {
|
|
231
|
+
await runExecFile("osascript", ["-e", script]);
|
|
232
|
+
logger("Chrome window hidden (Cmd-H)");
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
logger(`Failed to hide Chrome window: ${message}`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function setWindowsChromeWindowVisibility(pid, command, state, logger, runExecFile) {
|
|
242
|
+
const script = `
|
|
243
|
+
$ErrorActionPreference = 'Stop'
|
|
244
|
+
Add-Type @"
|
|
245
|
+
using System;
|
|
246
|
+
using System.Text;
|
|
247
|
+
using System.Runtime.InteropServices;
|
|
248
|
+
public static class AskProWindowVisibility {
|
|
249
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
250
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
251
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
|
252
|
+
[DllImport("user32.dll")] public static extern int GetClassName(IntPtr hWnd, StringBuilder value, int count);
|
|
253
|
+
[DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
|
|
254
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr GetProp(IntPtr hWnd, string name);
|
|
255
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool SetProp(IntPtr hWnd, string name, IntPtr value);
|
|
256
|
+
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
|
|
257
|
+
}
|
|
258
|
+
"@
|
|
259
|
+
$targetPid = [uint32]${pid}
|
|
260
|
+
$changed = $false
|
|
261
|
+
$preserved = $false
|
|
262
|
+
$callback = [AskProWindowVisibility+EnumWindowsProc]{
|
|
263
|
+
param([IntPtr]$hWnd, [IntPtr]$lParam)
|
|
264
|
+
$windowPid = [uint32]0
|
|
265
|
+
[void][AskProWindowVisibility]::GetWindowThreadProcessId($hWnd, [ref]$windowPid)
|
|
266
|
+
if ($windowPid -eq $targetPid -and [AskProWindowVisibility]::GetWindowTextLength($hWnd) -gt 0) {
|
|
267
|
+
$className = New-Object Text.StringBuilder 256
|
|
268
|
+
[void][AskProWindowVisibility]::GetClassName($hWnd, $className, $className.Capacity)
|
|
269
|
+
if ($className.ToString() -eq 'Chrome_WidgetWin_1') {
|
|
270
|
+
if (${command} -eq 0 -and [AskProWindowVisibility]::GetProp($hWnd, 'AskProHumanRecovery') -ne [IntPtr]::Zero) {
|
|
271
|
+
$script:preserved = $true
|
|
272
|
+
} else {
|
|
273
|
+
if (${command} -eq 9) {
|
|
274
|
+
[void][AskProWindowVisibility]::SetProp($hWnd, 'AskProHumanRecovery', [IntPtr]1)
|
|
275
|
+
}
|
|
276
|
+
[void][AskProWindowVisibility]::ShowWindowAsync($hWnd, ${command})
|
|
277
|
+
$script:changed = $true
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return $true
|
|
282
|
+
}
|
|
283
|
+
[void][AskProWindowVisibility]::EnumWindows($callback, [IntPtr]::Zero)
|
|
284
|
+
if ($changed) { [Console]::Out.Write('changed') }
|
|
285
|
+
elseif ($preserved) { [Console]::Out.Write('preserved') }
|
|
286
|
+
else { exit 2 }
|
|
287
|
+
`;
|
|
288
|
+
try {
|
|
289
|
+
const { stdout } = await runExecFile("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
290
|
+
windowsHide: true,
|
|
291
|
+
timeout: 5000,
|
|
292
|
+
});
|
|
293
|
+
if (String(stdout).trim() === "preserved") {
|
|
294
|
+
logger("[browser] Chrome window remains visible for human recovery");
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
logger(`[browser] Chrome window ${state} by pid`);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
logger(`[browser] Failed to set Chrome window ${state} by pid: ${message}`);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
export async function restoreChromeWindowByPid(pid, logger, deps = {}) {
|
|
307
|
+
const platform = deps.platform ?? process.platform;
|
|
308
|
+
const runExecFile = deps.execFileAsync ?? execFileAsync;
|
|
309
|
+
if (platform !== "win32" || !pid) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return setWindowsChromeWindowVisibility(pid, 9, "restored", logger, runExecFile);
|
|
313
|
+
}
|
|
314
|
+
export async function connectToChrome(port, logger, host) {
|
|
315
|
+
const client = await CDP({ port, host });
|
|
316
|
+
logger("Connected to Chrome DevTools protocol");
|
|
317
|
+
return client;
|
|
318
|
+
}
|
|
319
|
+
export async function connectToRemoteChrome(host, port, logger, targetUrl, browserWSEndpoint, options) {
|
|
320
|
+
if (browserWSEndpoint) {
|
|
321
|
+
return await connectToRemoteChromeTarget(host, port, logger, {
|
|
322
|
+
browserWSEndpoint,
|
|
323
|
+
targetUrl: targetUrl ?? "about:blank",
|
|
324
|
+
closeTargetOnDispose: true,
|
|
325
|
+
approvalWaitMs: options?.approvalWaitMs,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (targetUrl) {
|
|
329
|
+
const targetConnection = await connectToNewTarget(host, port, targetUrl, logger, {
|
|
330
|
+
opened: () => `Opened dedicated remote Chrome tab targeting ${targetUrl}`,
|
|
331
|
+
openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); falling back to first target.`,
|
|
332
|
+
attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); falling back to first target.`,
|
|
333
|
+
closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
|
|
334
|
+
});
|
|
335
|
+
if (targetConnection) {
|
|
336
|
+
return {
|
|
337
|
+
client: targetConnection.client,
|
|
338
|
+
targetId: targetConnection.targetId,
|
|
339
|
+
close: async () => {
|
|
340
|
+
await targetConnection.client.close().catch(() => undefined);
|
|
341
|
+
await closeRemoteChromeTarget(host, port, targetConnection.targetId, logger);
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const fallbackClient = await CDP({ host, port });
|
|
347
|
+
logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
|
|
348
|
+
return {
|
|
349
|
+
client: fallbackClient,
|
|
350
|
+
close: async () => {
|
|
351
|
+
await fallbackClient.close().catch(() => undefined);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export async function closeRemoteChromeTarget(host, port, targetId, logger) {
|
|
356
|
+
if (!targetId) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await CDP.Close({ host, port, id: targetId });
|
|
361
|
+
if (logger.verbose) {
|
|
362
|
+
logger(`Closed remote Chrome tab ${targetId}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
logger(`Failed to close remote Chrome tab ${targetId}: ${message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export async function listRemoteChromeTargets(options) {
|
|
371
|
+
if (!options.browserWSEndpoint) {
|
|
372
|
+
const targets = await CDP.List({ host: options.host, port: options.port });
|
|
373
|
+
return targets.map((target) => ({
|
|
374
|
+
...target,
|
|
375
|
+
targetId: target.targetId ?? target.id,
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
const browser = await CDP({ target: options.browserWSEndpoint, local: true });
|
|
379
|
+
try {
|
|
380
|
+
const result = await browser.Target.getTargets();
|
|
381
|
+
return (result.targetInfos ?? []).map((target) => ({
|
|
382
|
+
targetId: target.targetId,
|
|
383
|
+
type: target.type,
|
|
384
|
+
url: target.url,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
await browser.close().catch(() => undefined);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
392
|
+
if (!options.browserWSEndpoint) {
|
|
393
|
+
const client = await CDP({ host, port, target: options.targetId });
|
|
394
|
+
return {
|
|
395
|
+
client,
|
|
396
|
+
targetId: options.targetId,
|
|
397
|
+
close: async () => {
|
|
398
|
+
await client.close().catch(() => undefined);
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const browser = await connectToBrowserWebSocket(host, port, options.browserWSEndpoint, logger, options.approvalWaitMs);
|
|
403
|
+
let targetId = options.targetId;
|
|
404
|
+
try {
|
|
405
|
+
if (!targetId) {
|
|
406
|
+
const created = await browser.Target.createTarget({
|
|
407
|
+
url: options.targetUrl ?? "about:blank",
|
|
408
|
+
});
|
|
409
|
+
targetId = created.targetId;
|
|
410
|
+
logger(`Opened dedicated remote Chrome tab targeting ${options.targetUrl ?? "about:blank"}`);
|
|
411
|
+
}
|
|
412
|
+
const attached = await browser.Target.attachToTarget({ targetId, flatten: true });
|
|
413
|
+
const client = createSessionBoundChromeClient(browser, attached.sessionId);
|
|
414
|
+
return {
|
|
415
|
+
client,
|
|
416
|
+
targetId,
|
|
417
|
+
browserWSEndpoint: options.browserWSEndpoint,
|
|
418
|
+
close: async () => {
|
|
419
|
+
await browser.Target.detachFromTarget({ sessionId: attached.sessionId }).catch(() => undefined);
|
|
420
|
+
if (options.closeTargetOnDispose && targetId) {
|
|
421
|
+
await browser.Target.closeTarget({ targetId }).catch(() => undefined);
|
|
422
|
+
}
|
|
423
|
+
await browser.close().catch(() => undefined);
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
await browser.close().catch(() => undefined);
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function connectToBrowserWebSocket(host, port, browserWSEndpoint, logger, approvalWaitMs) {
|
|
433
|
+
const connectPromise = CDP({ target: browserWSEndpoint, local: true });
|
|
434
|
+
if (!approvalWaitMs || approvalWaitMs <= 0) {
|
|
435
|
+
return await connectPromise;
|
|
436
|
+
}
|
|
437
|
+
logger(`Waiting for Chrome remote debugging approval for ${host}:${port}...`);
|
|
438
|
+
let timeoutId = null;
|
|
439
|
+
try {
|
|
440
|
+
return await Promise.race([
|
|
441
|
+
connectPromise,
|
|
442
|
+
new Promise((_, reject) => {
|
|
443
|
+
timeoutId = setTimeout(() => {
|
|
444
|
+
reject(new Error(`ask-pro waited ${formatApprovalWait(approvalWaitMs)} for Chrome remote debugging approval at ${host}:${port}. Allow the Chrome prompt or retry after toggling remote debugging.`));
|
|
445
|
+
}, approvalWaitMs);
|
|
446
|
+
}),
|
|
447
|
+
]);
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
if (timeoutId) {
|
|
451
|
+
clearTimeout(timeoutId);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function formatApprovalWait(waitMs) {
|
|
456
|
+
if (waitMs % 1000 === 0) {
|
|
457
|
+
return `${waitMs / 1000}s`;
|
|
458
|
+
}
|
|
459
|
+
return `${waitMs}ms`;
|
|
460
|
+
}
|
|
461
|
+
async function connectToNewTarget(host, port, url, logger, messages) {
|
|
462
|
+
try {
|
|
463
|
+
const target = await CDP.New({ host, port, url });
|
|
464
|
+
try {
|
|
465
|
+
const client = await CDP({ host, port, target: target.id });
|
|
466
|
+
if (messages.opened) {
|
|
467
|
+
logger(messages.opened(target.id));
|
|
468
|
+
}
|
|
469
|
+
return { client, targetId: target.id };
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
473
|
+
logger(messages.attachFailed(target.id, message));
|
|
474
|
+
try {
|
|
475
|
+
await CDP.Close({ host, port, id: target.id });
|
|
476
|
+
}
|
|
477
|
+
catch (closeError) {
|
|
478
|
+
const closeMessage = closeError instanceof Error ? closeError.message : String(closeError);
|
|
479
|
+
logger(messages.closeFailed(target.id, closeMessage));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
485
|
+
logger(messages.openFailed(message));
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function createSessionBoundChromeClient(browser, sessionId) {
|
|
490
|
+
const browserWithEvents = browser;
|
|
491
|
+
const bindDomain = (domainName) => {
|
|
492
|
+
const domain = browser[domainName];
|
|
493
|
+
const eventName = (name) => `${domainName}.${name}.${sessionId}`;
|
|
494
|
+
return new Proxy((domain ?? {}), {
|
|
495
|
+
get(target, prop, receiver) {
|
|
496
|
+
if (prop === "on") {
|
|
497
|
+
return (name, listener) => {
|
|
498
|
+
const domainEvent = target[name];
|
|
499
|
+
if (typeof domainEvent === "function") {
|
|
500
|
+
return domainEvent(sessionId, listener);
|
|
501
|
+
}
|
|
502
|
+
browserWithEvents.on(eventName(name), listener);
|
|
503
|
+
return () => browserWithEvents.removeListener(eventName(name), listener);
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (prop === "off" || prop === "removeListener") {
|
|
507
|
+
return (name, listener) => {
|
|
508
|
+
const off = browserWithEvents.off ?? browserWithEvents.removeListener.bind(browserWithEvents);
|
|
509
|
+
off(eventName(name), listener);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const value = Reflect.get(target, prop, receiver);
|
|
513
|
+
if (typeof value !== "function") {
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
return (...args) => value(...args, sessionId);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
return {
|
|
521
|
+
...browser,
|
|
522
|
+
Network: bindDomain("Network"),
|
|
523
|
+
Page: bindDomain("Page"),
|
|
524
|
+
Runtime: bindDomain("Runtime"),
|
|
525
|
+
Input: bindDomain("Input"),
|
|
526
|
+
DOM: bindDomain("DOM"),
|
|
527
|
+
on: browserWithEvents.on.bind(browserWithEvents),
|
|
528
|
+
once: browserWithEvents.once.bind(browserWithEvents),
|
|
529
|
+
off: browserWithEvents.off?.bind(browserWithEvents) ??
|
|
530
|
+
browserWithEvents.removeListener.bind(browserWithEvents),
|
|
531
|
+
removeListener: browserWithEvents.removeListener.bind(browserWithEvents),
|
|
532
|
+
close: async () => {
|
|
533
|
+
await browser.Target.detachFromTarget({ sessionId }).catch(() => undefined);
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
export async function connectWithNewTab(port, logger, initialUrl, host, options) {
|
|
538
|
+
const effectiveHost = host ?? "127.0.0.1";
|
|
539
|
+
const url = initialUrl ?? "about:blank";
|
|
540
|
+
const fallbackToDefault = options?.fallbackToDefault ?? true;
|
|
541
|
+
const retries = Math.max(0, options?.retries ?? 0);
|
|
542
|
+
const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 250);
|
|
543
|
+
const fallbackLabel = fallbackToDefault
|
|
544
|
+
? "falling back to default target."
|
|
545
|
+
: "strict mode: not falling back.";
|
|
546
|
+
let attempt = 0;
|
|
547
|
+
while (attempt <= retries) {
|
|
548
|
+
const targetConnection = await connectToNewTarget(effectiveHost, port, url, logger, {
|
|
549
|
+
opened: (targetId) => `Opened isolated browser tab (target=${targetId})`,
|
|
550
|
+
openFailed: (message) => `Failed to open isolated browser tab (${message}); ${fallbackLabel}`,
|
|
551
|
+
attachFailed: (targetId, message) => `Failed to attach to isolated browser tab ${targetId} (${message}); ${fallbackLabel}`,
|
|
552
|
+
closeFailed: (targetId, message) => `Failed to close unused browser tab ${targetId}: ${message}`,
|
|
553
|
+
});
|
|
554
|
+
if (targetConnection) {
|
|
555
|
+
return targetConnection;
|
|
556
|
+
}
|
|
557
|
+
if (attempt >= retries) {
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
attempt += 1;
|
|
561
|
+
await delay(retryDelayMs * attempt);
|
|
562
|
+
}
|
|
563
|
+
if (!fallbackToDefault) {
|
|
564
|
+
throw new Error("Failed to open isolated browser tab; refusing to attach to default target.");
|
|
565
|
+
}
|
|
566
|
+
const client = await connectToChrome(port, logger, effectiveHost);
|
|
567
|
+
return { client };
|
|
568
|
+
}
|
|
569
|
+
export async function closeTab(port, targetId, logger, host) {
|
|
570
|
+
const effectiveHost = host ?? "127.0.0.1";
|
|
571
|
+
try {
|
|
572
|
+
await CDP.Close({ host: effectiveHost, port, id: targetId });
|
|
573
|
+
logger(`Closed isolated browser tab (target=${targetId})`);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
577
|
+
logger(`Failed to close browser tab ${targetId}: ${message}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
export function buildChromeFlags(headless, debugBindAddress, acceptLanguage = "en-US,en", options = {}) {
|
|
581
|
+
const primaryLanguage = acceptLanguage.split(",", 1)[0]?.trim() || "en-US";
|
|
582
|
+
const flags = [
|
|
583
|
+
"--disable-background-networking",
|
|
584
|
+
"--disable-breakpad",
|
|
585
|
+
"--disable-client-side-phishing-detection",
|
|
586
|
+
"--disable-default-apps",
|
|
587
|
+
"--disable-hang-monitor",
|
|
588
|
+
"--disable-popup-blocking",
|
|
589
|
+
"--disable-prompt-on-repost",
|
|
590
|
+
"--disable-sync",
|
|
591
|
+
"--disable-translate",
|
|
592
|
+
"--metrics-recording-only",
|
|
593
|
+
"--no-first-run",
|
|
594
|
+
"--safebrowsing-disable-auto-update",
|
|
595
|
+
"--disable-features=TranslateUI",
|
|
596
|
+
"--mute-audio",
|
|
597
|
+
"--window-size=1280,720",
|
|
598
|
+
`--lang=${primaryLanguage}`,
|
|
599
|
+
`--accept-lang=${acceptLanguage}`,
|
|
600
|
+
];
|
|
601
|
+
if (process.platform !== "win32" && !isWsl()) {
|
|
602
|
+
flags.push("--password-store=basic", "--use-mock-keychain");
|
|
603
|
+
}
|
|
604
|
+
if (debugBindAddress) {
|
|
605
|
+
flags.push(`--remote-debugging-address=${debugBindAddress}`);
|
|
606
|
+
}
|
|
607
|
+
if (headless) {
|
|
608
|
+
flags.push("--headless=new");
|
|
609
|
+
}
|
|
610
|
+
else if (options.startMinimized) {
|
|
611
|
+
flags.push("--start-minimized");
|
|
612
|
+
}
|
|
613
|
+
return flags;
|
|
614
|
+
}
|
|
615
|
+
export function buildChromeLaunchFlags(askProFlags) {
|
|
616
|
+
return [
|
|
617
|
+
...Launcher.defaultFlags().filter((flag) => !CPU_THROTTLING_OVERRIDE_FLAGS.has(flag)),
|
|
618
|
+
...askProFlags.filter((flag) => !CPU_THROTTLING_OVERRIDE_FLAGS.has(flag)),
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
export function shouldLaunchChromeMinimized(config, platform = process.platform) {
|
|
622
|
+
return (Boolean(config.startMinimized) &&
|
|
623
|
+
platform === "win32" &&
|
|
624
|
+
!config.headless &&
|
|
625
|
+
!config.hideWindow &&
|
|
626
|
+
!config.browserTabRef &&
|
|
627
|
+
!config.remoteChrome);
|
|
628
|
+
}
|
|
629
|
+
function parseDebugPortEnv() {
|
|
630
|
+
const raw = process.env.ASK_PRO_BROWSER_PORT ?? process.env.ASK_PRO_BROWSER_DEBUG_PORT;
|
|
631
|
+
if (!raw)
|
|
632
|
+
return null;
|
|
633
|
+
const value = Number.parseInt(raw, 10);
|
|
634
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
function resolveRemoteDebugHost() {
|
|
640
|
+
const override = process.env.ASK_PRO_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
|
|
641
|
+
if (override) {
|
|
642
|
+
return override;
|
|
643
|
+
}
|
|
644
|
+
if (!isWsl()) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const resolv = readFileSync("/etc/resolv.conf", "utf8");
|
|
649
|
+
for (const line of resolv.split("\n")) {
|
|
650
|
+
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
651
|
+
if (match?.[1]) {
|
|
652
|
+
return match[1];
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// ignore; fall back to localhost
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
function isWsl() {
|
|
662
|
+
if (process.platform !== "linux") {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (process.env.WSL_DISTRO_NAME) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
const release = os.release();
|
|
669
|
+
return release.toLowerCase().includes("microsoft");
|
|
670
|
+
}
|
|
671
|
+
const WINDOWS_BROKER_COMMAND_ENV = "ASK_PRO_WINDOWS_BROKER_COMMAND";
|
|
672
|
+
const WINDOWS_BROKER_CWD_ENV = "ASK_PRO_WINDOWS_BROKER_CWD";
|
|
673
|
+
const WINDOWS_BROKER_SCRIPT = `
|
|
674
|
+
$ErrorActionPreference = 'Stop'
|
|
675
|
+
$commandLine = [Environment]::GetEnvironmentVariable('${WINDOWS_BROKER_COMMAND_ENV}', 'Process')
|
|
676
|
+
if ([string]::IsNullOrWhiteSpace($commandLine)) { throw 'Missing brokered process command line.' }
|
|
677
|
+
$workingDirectory = [Environment]::GetEnvironmentVariable('${WINDOWS_BROKER_CWD_ENV}', 'Process')
|
|
678
|
+
$environment = @(
|
|
679
|
+
[Environment]::GetEnvironmentVariables('Process').GetEnumerator() |
|
|
680
|
+
Where-Object { $_.Key -notin @('${WINDOWS_BROKER_COMMAND_ENV}', '${WINDOWS_BROKER_CWD_ENV}') } |
|
|
681
|
+
ForEach-Object { "$($_.Key)=$($_.Value)" }
|
|
682
|
+
)
|
|
683
|
+
$startup = New-CimInstance -ClassName Win32_ProcessStartup -Property @{
|
|
684
|
+
EnvironmentVariables = [string[]]$environment
|
|
685
|
+
PriorityClass = [uint32][System.Diagnostics.ProcessPriorityClass]::BelowNormal
|
|
686
|
+
} -ClientOnly
|
|
687
|
+
$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
|
|
688
|
+
CommandLine = $commandLine
|
|
689
|
+
CurrentDirectory = $workingDirectory
|
|
690
|
+
ProcessStartupInformation = $startup
|
|
691
|
+
}
|
|
692
|
+
if ($result.ReturnValue -ne 0 -or $result.ProcessId -le 0) {
|
|
693
|
+
throw "Win32_Process.Create failed with return value $($result.ReturnValue)."
|
|
694
|
+
}
|
|
695
|
+
[Console]::Out.Write([string]$result.ProcessId)
|
|
696
|
+
`;
|
|
697
|
+
function quoteWindowsCommandLineArgument(value) {
|
|
698
|
+
let quoted = '"';
|
|
699
|
+
let backslashes = 0;
|
|
700
|
+
for (const character of value) {
|
|
701
|
+
if (character === "\\") {
|
|
702
|
+
backslashes += 1;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (character === '"') {
|
|
706
|
+
quoted += "\\".repeat(backslashes * 2 + 1);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
quoted += "\\".repeat(backslashes);
|
|
710
|
+
}
|
|
711
|
+
quoted += character;
|
|
712
|
+
backslashes = 0;
|
|
713
|
+
}
|
|
714
|
+
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
715
|
+
}
|
|
716
|
+
export function spawnWindowsProcessOutsideJob(command, args, options) {
|
|
717
|
+
const commandLine = [command, ...args].map(quoteWindowsCommandLineArgument).join(" ");
|
|
718
|
+
const cwd = typeof options.cwd === "string"
|
|
719
|
+
? options.cwd
|
|
720
|
+
: options.cwd
|
|
721
|
+
? fileURLToPath(options.cwd)
|
|
722
|
+
: process.cwd();
|
|
723
|
+
const broker = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", WINDOWS_BROKER_SCRIPT], {
|
|
724
|
+
encoding: "utf8",
|
|
725
|
+
env: {
|
|
726
|
+
...(options.env ?? process.env),
|
|
727
|
+
[WINDOWS_BROKER_COMMAND_ENV]: commandLine,
|
|
728
|
+
[WINDOWS_BROKER_CWD_ENV]: cwd,
|
|
729
|
+
},
|
|
730
|
+
timeout: 30_000,
|
|
731
|
+
windowsHide: true,
|
|
732
|
+
});
|
|
733
|
+
if (broker.error || broker.status !== 0) {
|
|
734
|
+
throw broker.error ?? new Error(broker.stderr?.trim() || "Windows process broker failed.");
|
|
735
|
+
}
|
|
736
|
+
const pid = Number.parseInt(broker.stdout.trim(), 10);
|
|
737
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
738
|
+
throw new Error("Windows process broker returned an invalid process id.");
|
|
739
|
+
}
|
|
740
|
+
return Object.assign(new EventEmitter(), {
|
|
741
|
+
pid,
|
|
742
|
+
stdio: [],
|
|
743
|
+
unref: () => undefined,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const spawnManagedChrome = ((command, args, options) => process.platform === "win32"
|
|
747
|
+
? spawnWindowsProcessOutsideJob(command, args, options)
|
|
748
|
+
: spawn(command, args, options));
|
|
749
|
+
async function launchManagedChrome({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
750
|
+
const launcher = new Launcher({
|
|
751
|
+
chromePath: chromePath ?? undefined,
|
|
752
|
+
chromeFlags,
|
|
753
|
+
userDataDir,
|
|
754
|
+
ignoreDefaultFlags: true,
|
|
755
|
+
handleSIGINT: false,
|
|
756
|
+
port: requestedPort ?? undefined,
|
|
757
|
+
}, { spawn: spawnManagedChrome });
|
|
758
|
+
if (host) {
|
|
759
|
+
const patched = launcher;
|
|
760
|
+
patched.isDebuggerReady = function patchedIsDebuggerReady() {
|
|
761
|
+
const debugPort = this.port ?? 0;
|
|
762
|
+
if (!debugPort) {
|
|
763
|
+
return Promise.reject(new Error("Missing Chrome debug port"));
|
|
764
|
+
}
|
|
765
|
+
return new Promise((resolve, reject) => {
|
|
766
|
+
const client = net.createConnection({ port: debugPort, host });
|
|
767
|
+
const cleanup = () => {
|
|
768
|
+
client.removeAllListeners();
|
|
769
|
+
client.end();
|
|
770
|
+
client.destroy();
|
|
771
|
+
client.unref();
|
|
772
|
+
};
|
|
773
|
+
client.once("error", (err) => {
|
|
774
|
+
cleanup();
|
|
775
|
+
reject(err);
|
|
776
|
+
});
|
|
777
|
+
client.once("connect", () => {
|
|
778
|
+
cleanup();
|
|
779
|
+
resolve();
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
await launcher.launch();
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
if (launcher.chromeProcess) {
|
|
789
|
+
try {
|
|
790
|
+
launcher.kill();
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// Preserve the launch failure.
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
const kill = async () => launcher.kill();
|
|
799
|
+
return {
|
|
800
|
+
pid: launcher.pid ?? undefined,
|
|
801
|
+
port: launcher.port ?? 0,
|
|
802
|
+
process: launcher.chromeProcess,
|
|
803
|
+
kill,
|
|
804
|
+
host: host ?? undefined,
|
|
805
|
+
remoteDebuggingPipes: launcher.remoteDebuggingPipes,
|
|
806
|
+
};
|
|
807
|
+
}
|