browser-debugger-cli 0.15.0 → 0.16.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/.claude/skills/bdg/SKILL.md +2 -1
- package/dist/cdp/methodTarget.d.ts +92 -0
- package/dist/cdp/methodTarget.js +159 -0
- package/dist/cdp/protocol.d.ts +16 -1
- package/dist/cdp/protocol.js +21 -0
- package/dist/cdp/schema.d.ts +55 -1
- package/dist/cdp/schema.js +134 -25
- package/dist/cdp/types.d.ts +3 -1
- package/dist/commands/cdp.d.ts +38 -1
- package/dist/commands/cdp.js +200 -133
- package/dist/commands/cleanup.js +18 -4
- package/dist/commands/dom/formInteraction.js +8 -4
- package/dist/commands/dom/helpers/index.d.ts +4 -4
- package/dist/commands/dom/helpers/index.js +3 -3
- package/dist/commands/dom/helpers/query.d.ts +2 -2
- package/dist/commands/dom/helpers/query.js +2 -2
- package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
- package/dist/commands/dom/helpers/screenshot.js +50 -668
- package/dist/commands/dom/screenshot.js +56 -36
- package/dist/commands/optionBehaviors.js +18 -8
- package/dist/commands/shared/CommandRunner.d.ts +5 -0
- package/dist/commands/shared/CommandRunner.js +18 -3
- package/dist/commands/shared/interrupt.d.ts +40 -0
- package/dist/commands/shared/interrupt.js +73 -0
- package/dist/commands/shared/optionTypes.d.ts +2 -0
- package/dist/commands/shared/startHelpers.d.ts +26 -3
- package/dist/commands/shared/startHelpers.js +145 -23
- package/dist/commands/types.d.ts +5 -0
- package/dist/connection/cdp.js +1 -16
- package/dist/connection/chromeIdentity.d.ts +24 -5
- package/dist/connection/chromeIdentity.js +53 -22
- package/dist/connection/launcher.d.ts +34 -1
- package/dist/connection/launcher.js +98 -10
- package/dist/connection/typed-cdp.d.ts +3 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/daemon/SessionController.d.ts +10 -5
- package/dist/daemon/SessionController.js +15 -8
- package/dist/daemon/ipcServer.js +1 -1
- package/dist/daemon/launcher.d.ts +5 -0
- package/dist/daemon/launcher.js +8 -1
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +9 -8
- package/dist/daemon/session/TelemetryStore.d.ts +5 -0
- package/dist/daemon/session/TelemetryStore.js +4 -0
- package/dist/daemon/session/captureGate.d.ts +59 -0
- package/dist/daemon/session/captureGate.js +96 -0
- package/dist/daemon/session/chromeConnection.d.ts +16 -1
- package/dist/daemon/session/chromeConnection.js +34 -4
- package/dist/daemon/session/collectors.d.ts +15 -0
- package/dist/daemon/session/collectors.js +39 -2
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +46 -11
- package/dist/daemon/session/downloads.d.ts +32 -0
- package/dist/daemon/session/downloads.js +96 -0
- package/dist/daemon/session/interactions.d.ts +3 -2
- package/dist/daemon/session/interactions.js +7 -2
- package/dist/daemon/session/plugins.js +6 -0
- package/dist/daemon.js +12843 -11482
- package/dist/errors/CommandError.d.ts +2 -0
- package/dist/errors/issues.d.ts +1 -1
- package/dist/errors/messages.d.ts +58 -0
- package/dist/errors/messages.js +112 -0
- package/dist/index.js +999 -1020
- package/dist/ipc/client.d.ts +14 -1
- package/dist/ipc/client.js +21 -4
- package/dist/ipc/protocol/commands.d.ts +32 -2
- package/dist/ipc/protocol/commands.js +1 -0
- package/dist/ipc/protocol/domTypes.d.ts +24 -1
- package/dist/ipc/session/queries.d.ts +3 -0
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/IPCError.d.ts +9 -0
- package/dist/ipc/transport/IPCError.js +12 -0
- package/dist/ipc/transport/errors.d.ts +2 -1
- package/dist/ipc/transport/errors.js +4 -1
- package/dist/ipc/transport/index.d.ts +4 -2
- package/dist/ipc/transport/index.js +13 -3
- package/dist/runtime/dom/actionEffects.d.ts +48 -9
- package/dist/runtime/dom/actionEffects.js +269 -34
- package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
- package/dist/runtime/dom/actionEffectsScripts.js +101 -2
- package/dist/runtime/dom/captureArea.d.ts +35 -0
- package/dist/runtime/dom/captureArea.js +203 -0
- package/dist/runtime/dom/elementInfo.d.ts +10 -8
- package/dist/runtime/dom/elementInfo.js +8 -6
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/page/bdgWorld.d.ts +9 -0
- package/dist/runtime/page/bdgWorld.js +11 -0
- package/dist/runtime/page/captureEmulation.d.ts +119 -0
- package/dist/runtime/page/captureEmulation.js +189 -0
- package/dist/runtime/page/captureScroll.d.ts +24 -0
- package/dist/runtime/page/captureScroll.js +124 -0
- package/dist/runtime/page/screenshot.d.ts +41 -0
- package/dist/runtime/page/screenshot.js +394 -0
- package/dist/session/paths.d.ts +14 -0
- package/dist/session/paths.js +25 -0
- package/dist/telemetry/downloads.d.ts +127 -0
- package/dist/telemetry/downloads.js +265 -0
- package/dist/telemetry/har/builder.js +22 -7
- package/dist/telemetry/har/sanitize.d.ts +7 -3
- package/dist/telemetry/har/sanitize.js +52 -6
- package/dist/telemetry/har/sanitizeBody.d.ts +47 -7
- package/dist/telemetry/har/sanitizeBody.js +429 -56
- package/dist/telemetry/har/types.d.ts +2 -0
- package/dist/telemetry/network.d.ts +4 -4
- package/dist/telemetry/network.js +38 -4
- package/dist/telemetry/networkRetention.d.ts +35 -14
- package/dist/telemetry/networkRetention.js +62 -26
- package/dist/types.d.ts +9 -14
- package/dist/ui/OutputBuilder.d.ts +3 -2
- package/dist/ui/OutputBuilder.js +4 -3
- package/dist/ui/formatters/cdp.d.ts +32 -9
- package/dist/ui/formatters/cdp.js +77 -6
- package/dist/ui/formatters/details.js +7 -15
- package/dist/ui/formatters/preview.d.ts +2 -0
- package/dist/ui/formatters/preview.js +7 -1
- package/dist/ui/formatters/status.js +6 -1
- package/dist/ui/formatting.d.ts +7 -0
- package/dist/ui/formatting.js +13 -0
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +13 -0
- package/dist/ui/messages/chrome.js +26 -0
- package/dist/ui/messages/commands.d.ts +71 -3
- package/dist/ui/messages/commands.js +98 -3
- package/dist/ui/messages/networkMessages.d.ts +24 -5
- package/dist/ui/messages/networkMessages.js +31 -8
- package/dist/utils/async.d.ts +3 -2
- package/dist/utils/async.js +16 -3
- package/dist/utils/http.d.ts +11 -4
- package/dist/utils/http.js +5 -3
- package/package.json +18 -4
- /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
- /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* Handles IPC communication with daemon to start browser sessions.
|
|
6
6
|
*/
|
|
7
7
|
import { timeoutError } from './CommandRunner.js';
|
|
8
|
+
import { interruptExitCode, interruptSignal, } from './interrupt.js';
|
|
8
9
|
import { landingPage } from './landingPage.js';
|
|
9
10
|
import { DaemonError, SessionDirError } from '../../daemon/errors.js';
|
|
10
11
|
import { launchDaemon } from '../../daemon/launcher.js';
|
|
11
12
|
import { LAUNCHED_CHROME_DESCRIPTION, sessionAlreadyRunningError, alreadyRunningSuggestion, sessionTargetMismatchError, daemonNotRunningError, invalidResponseError, genericError, } from '../../errors/messages.js';
|
|
12
|
-
import { startSession as sendStartSessionRequest } from '../../ipc/client.js';
|
|
13
|
+
import { startSession as sendStartSessionRequest, stopSession as sendStopSessionRequest, } from '../../ipc/client.js';
|
|
13
14
|
import { IPCErrorCode, } from '../../ipc/index.js';
|
|
14
15
|
import { IPCTimeoutError } from '../../ipc/transport/index.js';
|
|
15
16
|
import { isConnectionError } from '../../ipc/utils/errors.js';
|
|
@@ -24,12 +25,14 @@ import { getExitCodeForIPCError } from '../../utils/errorMapping.js';
|
|
|
24
25
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
25
26
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
26
27
|
import { filterDefined } from '../../utils/objects.js';
|
|
28
|
+
import { isProcessAlive } from '../../utils/process.js';
|
|
27
29
|
const log = createLogger('bdg');
|
|
28
30
|
/** How long a failed start waits for the daemons it spawned to exit */
|
|
29
31
|
export const SPAWNED_DAEMON_EXIT_WAIT_MS = 3000;
|
|
30
32
|
const DEFAULT_START_DEPS = {
|
|
31
33
|
launch: launchDaemon,
|
|
32
34
|
send: sendStartSessionRequest,
|
|
35
|
+
stop: sendStopSessionRequest,
|
|
33
36
|
exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS,
|
|
34
37
|
};
|
|
35
38
|
/**
|
|
@@ -38,30 +41,55 @@ const DEFAULT_START_DEPS = {
|
|
|
38
41
|
* Spawns the daemon if needed, sends `start_session_request`, then prints the
|
|
39
42
|
* result as a JSON envelope (`--json`) or human-readable text, and exits.
|
|
40
43
|
*
|
|
44
|
+
* Ctrl-C (or SIGTERM) cancels the start at any point until the outcome is
|
|
45
|
+
* printed: the connection is closed, so the daemon abandons the session (a
|
|
46
|
+
* daemon spawned but not yet asked is told to shut down, a session that has
|
|
47
|
+
* just started is stopped), and the command exits with 130 (143) once the
|
|
48
|
+
* daemon it spawned has exited (bounded, like a failed start), so a command
|
|
49
|
+
* run right after sees no session. A second signal exits at once.
|
|
50
|
+
*
|
|
51
|
+
* Race-free: {@link attemptStart} checks the interrupt last, synchronously,
|
|
52
|
+
* and only microtasks run between that check and the exit in
|
|
53
|
+
* {@link reportStartOutcome}; a signal handler runs as a macrotask, so a
|
|
54
|
+
* signal either lands before the check (the start is cancelled) or after the
|
|
55
|
+
* outcome has been printed.
|
|
56
|
+
*
|
|
41
57
|
* @param url - Target URL to navigate to
|
|
42
58
|
* @param options - Session configuration options
|
|
43
59
|
* @param telemetry - Array of telemetry types to enable
|
|
44
60
|
*/
|
|
45
61
|
export async function startSessionViaDaemon(url, options, telemetry) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
const interrupt = new AbortController();
|
|
63
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
64
|
+
process.on(signal, () => {
|
|
65
|
+
if (interrupt.signal.aborted)
|
|
66
|
+
reportStartOutcome(interruptedOutcome(signal), options);
|
|
67
|
+
interrupt.abort(signal);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
reportStartOutcome(await attemptStart(url, options, telemetry, DEFAULT_START_DEPS, interrupt.signal), options);
|
|
49
71
|
}
|
|
50
72
|
/**
|
|
51
73
|
* Start a session, retrying while the previous session shuts down. After a
|
|
52
74
|
* failure the daemon reported (or a dropped connection), it waits for every
|
|
53
75
|
* daemon the attempts spawned to exit ({@link afterSpawnedDaemonExit}).
|
|
54
76
|
*
|
|
77
|
+
* An interrupt cancels the start whenever it arrives: a start that succeeded
|
|
78
|
+
* meanwhile is stopped ({@link cancelStartedSession}), and a failure keeps its
|
|
79
|
+
* message but exits 130 (143), also when the signal came during the wait.
|
|
80
|
+
*
|
|
55
81
|
* @param url - Target URL
|
|
56
82
|
* @param options - Session options
|
|
57
83
|
* @param telemetry - Telemetry types
|
|
58
84
|
* @param deps - How to reach the daemon (tests replace it)
|
|
59
|
-
* @
|
|
85
|
+
* @param interrupt - Aborted (reason: the signal) on Ctrl-C or SIGTERM: the
|
|
86
|
+
* start is cancelled and the daemon it spawned waited for like after a failure
|
|
87
|
+
* @returns Start outcome, ready to report (the interrupt is checked last)
|
|
60
88
|
*/
|
|
61
|
-
export async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
|
|
89
|
+
export async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS, interrupt) {
|
|
62
90
|
const spawned = [];
|
|
63
91
|
const attempt = async () => {
|
|
64
|
-
const outcome = await requestSession(url, options, telemetry, deps);
|
|
92
|
+
const outcome = await requestSession(url, options, telemetry, deps, interrupt);
|
|
65
93
|
if (!outcome.ok && outcome.spawned)
|
|
66
94
|
spawned.push(outcome.spawned);
|
|
67
95
|
return outcome;
|
|
@@ -69,10 +97,69 @@ export async function attemptStart(url, options, telemetry, deps = DEFAULT_START
|
|
|
69
97
|
let outcome = await attempt();
|
|
70
98
|
const deadline = Date.now() + SHUTDOWN_WAIT_MS;
|
|
71
99
|
while (isShuttingDown(outcome) && Date.now() < deadline) {
|
|
72
|
-
await delay(SHUTDOWN_POLL_MS);
|
|
100
|
+
await delay(SHUTDOWN_POLL_MS, interrupt);
|
|
73
101
|
outcome = await attempt();
|
|
74
102
|
}
|
|
75
|
-
return
|
|
103
|
+
return settleOutcome(outcome, spawned, deps, interrupt);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Settle the last attempt's outcome: a success is returned as is, or stopped
|
|
107
|
+
* when interrupted; a failure waits for the daemons the attempts spawned and
|
|
108
|
+
* takes the signal's exit code when interrupted.
|
|
109
|
+
*
|
|
110
|
+
* Race-free: for a success the interrupt is checked last, with no await
|
|
111
|
+
* between the check and the return (see {@link startSessionViaDaemon}).
|
|
112
|
+
*
|
|
113
|
+
* @param outcome - The last attempt's outcome
|
|
114
|
+
* @param spawned - Exiting daemons the attempts spawned
|
|
115
|
+
* @param deps - How to reach the daemon
|
|
116
|
+
* @param interrupt - Aborted on Ctrl-C or SIGTERM
|
|
117
|
+
* @returns Start outcome, ready to report
|
|
118
|
+
*/
|
|
119
|
+
async function settleOutcome(outcome, spawned, deps, interrupt) {
|
|
120
|
+
if (outcome.ok) {
|
|
121
|
+
const { spawned: daemon, ...started } = outcome;
|
|
122
|
+
return interrupt?.aborted
|
|
123
|
+
? cancelStartedSession(started.data, daemon, deps, interrupt)
|
|
124
|
+
: started;
|
|
125
|
+
}
|
|
126
|
+
const failure = await afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
|
|
127
|
+
return interrupt?.aborted ? withInterruptExitCode(failure, interrupt) : failure;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Cancel a start that succeeded after Ctrl-C (the response came before the
|
|
131
|
+
* interrupt closed the connection): the session is stopped, since nobody
|
|
132
|
+
* learns that it exists, and its daemon waited for like after a failure.
|
|
133
|
+
*
|
|
134
|
+
* @param data - The started session
|
|
135
|
+
* @param spawned - The daemon this attempt spawned, if any
|
|
136
|
+
* @param deps - How to reach the daemon
|
|
137
|
+
* @param interrupt - The aborted interrupt
|
|
138
|
+
* @returns The cancelled outcome (130, or 143 for SIGTERM)
|
|
139
|
+
*/
|
|
140
|
+
function cancelStartedSession(data, spawned, deps, interrupt) {
|
|
141
|
+
log.debug('Interrupted as the session started; stopping it');
|
|
142
|
+
deps.stop().catch((error) => {
|
|
143
|
+
log.debug(`Stopping the interrupted session failed: ${getErrorMessage(error)}`);
|
|
144
|
+
});
|
|
145
|
+
const daemon = spawned ?? {
|
|
146
|
+
pid: data.daemonPid,
|
|
147
|
+
hasExited: () => !isProcessAlive(data.daemonPid),
|
|
148
|
+
};
|
|
149
|
+
return afterSpawnedDaemonExit(cancelledOutcome(interrupt), [daemon], deps.exitWaitMs);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A failed start that was also interrupted: its message is kept (it says what
|
|
153
|
+
* went wrong), its exit code is the signal's.
|
|
154
|
+
*
|
|
155
|
+
* @param outcome - Failed start outcome
|
|
156
|
+
* @param interrupt - The aborted interrupt
|
|
157
|
+
* @returns The outcome exiting with 130 (143 for SIGTERM)
|
|
158
|
+
*/
|
|
159
|
+
function withInterruptExitCode(outcome, interrupt) {
|
|
160
|
+
return outcome.ok
|
|
161
|
+
? outcome
|
|
162
|
+
: { ...outcome, exitCode: interruptExitCode(interruptSignal(interrupt)) };
|
|
76
163
|
}
|
|
77
164
|
/**
|
|
78
165
|
* Let the daemons a failed start spawned finish exiting before the error is
|
|
@@ -110,16 +197,24 @@ export async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
|
|
|
110
197
|
};
|
|
111
198
|
}
|
|
112
199
|
/**
|
|
113
|
-
* The outcome of a start interrupted with Ctrl-C (the daemon
|
|
114
|
-
* closed connection and cancels the start).
|
|
200
|
+
* The outcome of a start interrupted with Ctrl-C or SIGTERM (the daemon
|
|
201
|
+
* notices the closed connection and cancels the start).
|
|
115
202
|
*
|
|
116
203
|
* @param signal - The signal that stopped the start
|
|
117
|
-
* @returns Failed start outcome (exit 130)
|
|
204
|
+
* @returns Failed start outcome (exit 130 or 143)
|
|
118
205
|
*/
|
|
119
206
|
function interruptedOutcome(signal) {
|
|
120
207
|
const message = `Start cancelled (${signal === 'SIGINT' ? 'interrupted' : 'terminated'})`;
|
|
121
|
-
|
|
122
|
-
|
|
208
|
+
return { ok: false, error: message, human: message, exitCode: interruptExitCode(signal) };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The outcome for a start whose interrupt signal has aborted.
|
|
212
|
+
*
|
|
213
|
+
* @param interrupt - Aborted interrupt signal (reason: the signal name)
|
|
214
|
+
* @returns Failed start outcome (exit 130, or 143 for SIGTERM)
|
|
215
|
+
*/
|
|
216
|
+
function cancelledOutcome(interrupt) {
|
|
217
|
+
return interruptedOutcome(interruptSignal(interrupt));
|
|
123
218
|
}
|
|
124
219
|
/** How long a new start waits for the previous session to finish shutting down */
|
|
125
220
|
const SHUTDOWN_WAIT_MS = 15000;
|
|
@@ -144,9 +239,15 @@ function isShuttingDown(outcome) {
|
|
|
144
239
|
* @param url - Target URL
|
|
145
240
|
* @param options - Session options
|
|
146
241
|
* @param telemetry - Telemetry types
|
|
147
|
-
* @
|
|
242
|
+
* @param deps - How to reach the daemon
|
|
243
|
+
* @param interrupt - Cancels the start when aborted
|
|
244
|
+
* @returns Start outcome, with the daemon it spawned for a started session or
|
|
245
|
+
* an exiting daemon; interrupted before the request was sent, a daemon it
|
|
246
|
+
* spawned is told to shut down (it hosts nothing) and waited for
|
|
148
247
|
*/
|
|
149
|
-
async function requestSession(url, options, telemetry, deps) {
|
|
248
|
+
async function requestSession(url, options, telemetry, deps, interrupt) {
|
|
249
|
+
if (interrupt?.aborted)
|
|
250
|
+
return cancelledOutcome(interrupt);
|
|
150
251
|
let spawned;
|
|
151
252
|
try {
|
|
152
253
|
spawned = await deps.launch();
|
|
@@ -165,8 +266,25 @@ async function requestSession(url, options, telemetry, deps) {
|
|
|
165
266
|
const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
|
|
166
267
|
return { ok: false, error: message, human: genericError(message), exitCode };
|
|
167
268
|
}
|
|
168
|
-
|
|
169
|
-
|
|
269
|
+
if (interrupt?.aborted)
|
|
270
|
+
return cancelBeforeRequest(spawned, interrupt);
|
|
271
|
+
const outcome = await sendStart(url, options, telemetry, deps.send, interrupt);
|
|
272
|
+
return spawned && (outcome.ok || outcome.daemonExiting) ? { ...outcome, spawned } : outcome;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Cancel a start interrupted while its daemon was being spawned: the daemon
|
|
276
|
+
* would otherwise sit idle (and answer) until its idle timeout, so it is told
|
|
277
|
+
* to shut down and waited for like after a failure.
|
|
278
|
+
*
|
|
279
|
+
* @param spawned - The daemon this attempt spawned (undefined: one was already running)
|
|
280
|
+
* @param interrupt - The aborted interrupt
|
|
281
|
+
* @returns The cancelled outcome
|
|
282
|
+
*/
|
|
283
|
+
function cancelBeforeRequest(spawned, interrupt) {
|
|
284
|
+
if (!spawned)
|
|
285
|
+
return cancelledOutcome(interrupt);
|
|
286
|
+
spawned.stop();
|
|
287
|
+
return { ...cancelledOutcome(interrupt), daemonExiting: true, spawned };
|
|
170
288
|
}
|
|
171
289
|
/**
|
|
172
290
|
* Ask the running daemon to start a session.
|
|
@@ -175,11 +293,13 @@ async function requestSession(url, options, telemetry, deps) {
|
|
|
175
293
|
* @param options - Session options
|
|
176
294
|
* @param telemetry - Telemetry types
|
|
177
295
|
* @param send - Sends the request
|
|
178
|
-
* @
|
|
179
|
-
*
|
|
180
|
-
*
|
|
296
|
+
* @param interrupt - Cancels the request when aborted
|
|
297
|
+
* @returns Start outcome; `daemonExiting` for a failure the daemon reported,
|
|
298
|
+
* a dropped connection or a cancelled start (the daemon abandons it), not
|
|
299
|
+
* for a timeout or an unexpected error (the daemon may still be starting
|
|
300
|
+
* the session then)
|
|
181
301
|
*/
|
|
182
|
-
async function sendStart(url, options, telemetry, send) {
|
|
302
|
+
async function sendStart(url, options, telemetry, send, interrupt) {
|
|
183
303
|
try {
|
|
184
304
|
log.debug('Connecting to daemon...');
|
|
185
305
|
const response = await send(url, filterDefined({
|
|
@@ -194,7 +314,7 @@ async function sendStart(url, options, telemetry, send) {
|
|
|
194
314
|
chromeFlags: options.chromeFlags,
|
|
195
315
|
viewport: options.viewport,
|
|
196
316
|
colorScheme: options.colorScheme,
|
|
197
|
-
}));
|
|
317
|
+
}), interrupt);
|
|
198
318
|
if (response.status === 'error') {
|
|
199
319
|
return { ...describeStartFailure(response, options), daemonExiting: true };
|
|
200
320
|
}
|
|
@@ -210,6 +330,8 @@ async function sendStart(url, options, telemetry, send) {
|
|
|
210
330
|
return { ok: true, data: response.data };
|
|
211
331
|
}
|
|
212
332
|
catch (error) {
|
|
333
|
+
if (interrupt?.aborted)
|
|
334
|
+
return { ...cancelledOutcome(interrupt), daemonExiting: true };
|
|
213
335
|
if (error instanceof IPCTimeoutError) {
|
|
214
336
|
const timeout = timeoutError(error);
|
|
215
337
|
return {
|
package/dist/commands/types.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export interface CleanupResult {
|
|
|
36
36
|
};
|
|
37
37
|
/** Directory deleted by `--purge` */
|
|
38
38
|
purged?: string;
|
|
39
|
+
/** Downloaded files cleanup left in place (not with `--purge`) */
|
|
40
|
+
downloadsKept?: {
|
|
41
|
+
dir: string;
|
|
42
|
+
files: number;
|
|
43
|
+
};
|
|
39
44
|
message: string;
|
|
40
45
|
warnings?: string[];
|
|
41
46
|
}
|
package/dist/connection/cdp.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
1
|
import WebSocket from 'ws';
|
|
3
2
|
import { delay as asyncDelay } from '../utils/async.js';
|
|
4
3
|
import { getErrorMessage } from '../utils/errors.js';
|
|
@@ -16,20 +15,6 @@ const NO_PONG_RECEIVED_REASON = 'No pong received';
|
|
|
16
15
|
const NORMAL_CLOSURE_REASON = 'Normal closure';
|
|
17
16
|
const CONNECTION_ATTEMPT_FAILED_MESSAGE = (attempt, delay) => `Connection attempt ${attempt + 1} failed, retrying in ${delay}ms...`;
|
|
18
17
|
const CONNECTION_ABORTED_ERROR = 'Connection attempts aborted';
|
|
19
|
-
/**
|
|
20
|
-
* Wait between connection attempts, ending early when `signal` aborts.
|
|
21
|
-
*
|
|
22
|
-
* @param ms - Delay in milliseconds
|
|
23
|
-
* @param signal - Optional abort signal
|
|
24
|
-
*/
|
|
25
|
-
async function abortableDelay(ms, signal) {
|
|
26
|
-
if (!signal)
|
|
27
|
-
return asyncDelay(ms);
|
|
28
|
-
await sleep(ms, undefined, { signal }).catch((error) => {
|
|
29
|
-
if (!signal.aborted)
|
|
30
|
-
throw error;
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
18
|
const FAILED_CONNECT_ATTEMPTS_ERROR = (maxRetries, lastErrorMessage) => `Failed to connect after ${maxRetries} attempts: ${lastErrorMessage}`;
|
|
34
19
|
const WEBSOCKET_CLOSED_MESSAGE = (code, reason) => `WebSocket closed: ${code} - ${reason}`;
|
|
35
20
|
const RECONNECTING_MESSAGE = (delay, attempt, maxAttempts) => `Reconnecting in ${delay}ms... (attempt ${attempt}/${maxAttempts})`;
|
|
@@ -130,7 +115,7 @@ export class CDPConnection {
|
|
|
130
115
|
if (attempt < maxRetries - 1) {
|
|
131
116
|
const delay = this.calculateBackoffDelay(attempt, this.config.maxRetryDelay);
|
|
132
117
|
this.logger.info(CONNECTION_ATTEMPT_FAILED_MESSAGE(attempt, delay));
|
|
133
|
-
await
|
|
118
|
+
await asyncDelay(delay, options.signal);
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
121
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* on 127.0.0.1 and nothing listens on [::1]: the port was free on both right
|
|
18
18
|
* before the launch, and a second listener is how a conflict shows.
|
|
19
19
|
*/
|
|
20
|
+
import { ChromeLaunchError } from './errors.js';
|
|
20
21
|
import { type StartupLogs } from './startupExit.js';
|
|
21
22
|
/**
|
|
22
23
|
* The debugging endpoint a Chrome announced for itself.
|
|
@@ -42,9 +43,24 @@ export declare function parseDevToolsListening(lines: readonly string[]): DevToo
|
|
|
42
43
|
* @param logs - Chrome's log positions from before the launch
|
|
43
44
|
* @param isRunning - Whether Chrome is still running (waiting stops when not)
|
|
44
45
|
* @param timeoutMs - Longest wait
|
|
45
|
-
* @
|
|
46
|
+
* @param signal - Ends the wait early when aborted
|
|
47
|
+
* @returns Endpoint, or null if Chrome exited, announced none in time or the
|
|
48
|
+
* wait was aborted
|
|
46
49
|
*/
|
|
47
|
-
export declare function waitForDevToolsEndpoint(logs: StartupLogs, isRunning: () => boolean, timeoutMs?: number): Promise<DevToolsEndpoint | null>;
|
|
50
|
+
export declare function waitForDevToolsEndpoint(logs: StartupLogs, isRunning: () => boolean, timeoutMs?: number, signal?: AbortSignal): Promise<DevToolsEndpoint | null>;
|
|
51
|
+
/**
|
|
52
|
+
* The error for a launch that was aborted (the session was stopped).
|
|
53
|
+
*
|
|
54
|
+
* @returns Launch error
|
|
55
|
+
*/
|
|
56
|
+
export declare function launchAbortedError(): ChromeLaunchError;
|
|
57
|
+
/**
|
|
58
|
+
* End a launch whose session was stopped.
|
|
59
|
+
*
|
|
60
|
+
* @param signal - The launch's abort signal
|
|
61
|
+
* @throws ChromeLaunchError if the signal is aborted
|
|
62
|
+
*/
|
|
63
|
+
export declare function throwIfLaunchAborted(signal: AbortSignal | undefined): void;
|
|
48
64
|
/**
|
|
49
65
|
* Check that the Chrome answering on 127.0.0.1:<port> is the one just
|
|
50
66
|
* launched, so bdg never drives another session's browser.
|
|
@@ -55,9 +71,11 @@ export declare function waitForDevToolsEndpoint(logs: StartupLogs, isRunning: ()
|
|
|
55
71
|
* that fell back to [::1] is asked once: 127.0.0.1 is held by something else.
|
|
56
72
|
*
|
|
57
73
|
* @param options - Chrome's log positions from before the launch, requested
|
|
58
|
-
* port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID
|
|
59
|
-
* longest wait for its announcement and its answer
|
|
60
|
-
*
|
|
74
|
+
* port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID,
|
|
75
|
+
* longest wait for its announcement and its answer, and a signal that ends
|
|
76
|
+
* the waits and a pending request at once (the session was stopped)
|
|
77
|
+
* @throws ChromeLaunchError: without an issue if `signal` aborts;
|
|
78
|
+
* CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
|
|
61
79
|
* PORT_IN_USE if another browser or process answers on the port;
|
|
62
80
|
* CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers, or
|
|
63
81
|
* announced the port but did not answer on it in time
|
|
@@ -67,5 +85,6 @@ export declare function verifyLaunchedChrome(options: {
|
|
|
67
85
|
port: number;
|
|
68
86
|
pid: number;
|
|
69
87
|
timeoutMs?: number;
|
|
88
|
+
signal?: AbortSignal | undefined;
|
|
70
89
|
}): Promise<void>;
|
|
71
90
|
//# sourceMappingURL=chromeIdentity.d.ts.map
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* before the launch, and a second listener is how a conflict shows.
|
|
19
19
|
*/
|
|
20
20
|
import { createLogger } from '../ui/logging/index.js';
|
|
21
|
-
import { chromeNotAnsweringReason, portTakenByReason } from '../ui/messages/chrome.js';
|
|
21
|
+
import { CHROME_LAUNCH_ABORTED_MESSAGE, chromeNotAnsweringReason, portTakenByReason, } from '../ui/messages/chrome.js';
|
|
22
22
|
import { delay } from '../utils/async.js';
|
|
23
23
|
import { CDP_HTTP_TIMEOUT_MS, fetchBrowserWsUrl, probeDevToolsEndpoint, } from '../utils/http.js';
|
|
24
24
|
import { isProcessAlive } from '../utils/process.js';
|
|
@@ -63,17 +63,37 @@ export function parseDevToolsListening(lines) {
|
|
|
63
63
|
* @param logs - Chrome's log positions from before the launch
|
|
64
64
|
* @param isRunning - Whether Chrome is still running (waiting stops when not)
|
|
65
65
|
* @param timeoutMs - Longest wait
|
|
66
|
-
* @
|
|
66
|
+
* @param signal - Ends the wait early when aborted
|
|
67
|
+
* @returns Endpoint, or null if Chrome exited, announced none in time or the
|
|
68
|
+
* wait was aborted
|
|
67
69
|
*/
|
|
68
|
-
export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPOINT_WAIT_MS) {
|
|
70
|
+
export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPOINT_WAIT_MS, signal) {
|
|
69
71
|
const deadline = Date.now() + timeoutMs;
|
|
70
72
|
for (;;) {
|
|
71
73
|
const endpoint = parseDevToolsListening(readStartupLines(logs));
|
|
72
|
-
if (endpoint || !isRunning() || Date.now() >= deadline)
|
|
74
|
+
if (endpoint || !isRunning() || Date.now() >= deadline || signal?.aborted)
|
|
73
75
|
return endpoint;
|
|
74
|
-
await delay(ENDPOINT_POLL_MS);
|
|
76
|
+
await delay(ENDPOINT_POLL_MS, signal);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The error for a launch that was aborted (the session was stopped).
|
|
81
|
+
*
|
|
82
|
+
* @returns Launch error
|
|
83
|
+
*/
|
|
84
|
+
export function launchAbortedError() {
|
|
85
|
+
return new ChromeLaunchError(CHROME_LAUNCH_ABORTED_MESSAGE);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* End a launch whose session was stopped.
|
|
89
|
+
*
|
|
90
|
+
* @param signal - The launch's abort signal
|
|
91
|
+
* @throws ChromeLaunchError if the signal is aborted
|
|
92
|
+
*/
|
|
93
|
+
export function throwIfLaunchAborted(signal) {
|
|
94
|
+
if (signal?.aborted)
|
|
95
|
+
throw launchAbortedError();
|
|
96
|
+
}
|
|
77
97
|
/**
|
|
78
98
|
* Check that the Chrome answering on 127.0.0.1:<port> is the one just
|
|
79
99
|
* launched, so bdg never drives another session's browser.
|
|
@@ -84,28 +104,32 @@ export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPO
|
|
|
84
104
|
* that fell back to [::1] is asked once: 127.0.0.1 is held by something else.
|
|
85
105
|
*
|
|
86
106
|
* @param options - Chrome's log positions from before the launch, requested
|
|
87
|
-
* port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID
|
|
88
|
-
* longest wait for its announcement and its answer
|
|
89
|
-
*
|
|
107
|
+
* port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID,
|
|
108
|
+
* longest wait for its announcement and its answer, and a signal that ends
|
|
109
|
+
* the waits and a pending request at once (the session was stopped)
|
|
110
|
+
* @throws ChromeLaunchError: without an issue if `signal` aborts;
|
|
111
|
+
* CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
|
|
90
112
|
* PORT_IN_USE if another browser or process answers on the port;
|
|
91
113
|
* CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers, or
|
|
92
114
|
* announced the port but did not answer on it in time
|
|
93
115
|
*/
|
|
94
116
|
export async function verifyLaunchedChrome(options) {
|
|
95
|
-
const { logs, port, pid, timeoutMs = ENDPOINT_WAIT_MS } = options;
|
|
117
|
+
const { logs, port, pid, timeoutMs = ENDPOINT_WAIT_MS, signal } = options;
|
|
96
118
|
const started = Date.now();
|
|
97
119
|
const isRunning = () => isProcessAlive(pid);
|
|
98
|
-
const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs);
|
|
120
|
+
const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs, signal);
|
|
121
|
+
throwIfLaunchAborted(signal);
|
|
99
122
|
if (!endpoint && !isRunning())
|
|
100
123
|
throw diedError(port, pid);
|
|
101
124
|
if (!endpoint)
|
|
102
|
-
return acceptUnannouncedChrome(logs, port);
|
|
125
|
+
return acceptUnannouncedChrome(logs, port, signal);
|
|
103
126
|
if (endpoint.port !== port) {
|
|
104
127
|
throw portTakenError(port, `Chrome listens on port ${endpoint.port} instead`);
|
|
105
128
|
}
|
|
106
129
|
const onLoopback = endpoint.host === '127.0.0.1';
|
|
107
130
|
const deadline = onLoopback ? started + timeoutMs : Date.now();
|
|
108
|
-
const answer = await waitForAnswer(port, deadline, isRunning);
|
|
131
|
+
const answer = await waitForAnswer(port, deadline, isRunning, signal);
|
|
132
|
+
throwIfLaunchAborted(signal);
|
|
109
133
|
if (answer.kind === 'devtools' && new URL(answer.wsUrl).pathname === endpoint.browserPath) {
|
|
110
134
|
log.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
|
|
111
135
|
return;
|
|
@@ -119,22 +143,25 @@ export async function verifyLaunchedChrome(options) {
|
|
|
119
143
|
}
|
|
120
144
|
/**
|
|
121
145
|
* Ask 127.0.0.1:<port> for its DevTools version until something answers,
|
|
122
|
-
* Chrome exits
|
|
123
|
-
*
|
|
146
|
+
* Chrome exits, the deadline passes (at least once) or `signal` aborts (which
|
|
147
|
+
* also ends a pending request). Each request waits at most until the deadline
|
|
148
|
+
* (but at least MIN_ANSWER_WAIT_MS).
|
|
124
149
|
*
|
|
125
150
|
* @param port - Requested port
|
|
126
151
|
* @param deadline - Time (ms since epoch) to stop asking
|
|
127
152
|
* @param isRunning - Whether Chrome is still running
|
|
153
|
+
* @param signal - Ends the wait early when aborted
|
|
128
154
|
* @returns The first answer, or the last `unreachable` result
|
|
129
155
|
*/
|
|
130
|
-
async function waitForAnswer(port, deadline, isRunning) {
|
|
156
|
+
async function waitForAnswer(port, deadline, isRunning, signal) {
|
|
131
157
|
for (;;) {
|
|
132
158
|
const remaining = Math.max(deadline - Date.now(), MIN_ANSWER_WAIT_MS);
|
|
133
159
|
const timeoutMs = Math.min(CDP_HTTP_TIMEOUT_MS, remaining);
|
|
134
|
-
const answer = await probeDevToolsEndpoint(port, undefined, { timeoutMs });
|
|
135
|
-
|
|
160
|
+
const answer = await probeDevToolsEndpoint(port, undefined, { timeoutMs, signal });
|
|
161
|
+
const done = answer.kind !== 'unreachable' || !isRunning() || Date.now() >= deadline;
|
|
162
|
+
if (done || signal?.aborted)
|
|
136
163
|
return answer;
|
|
137
|
-
await delay(ENDPOINT_POLL_MS);
|
|
164
|
+
await delay(ENDPOINT_POLL_MS, signal);
|
|
138
165
|
}
|
|
139
166
|
}
|
|
140
167
|
/**
|
|
@@ -155,13 +182,17 @@ function answerSource(answer) {
|
|
|
155
182
|
*
|
|
156
183
|
* @param logs - Chrome's log positions (the stderr log is named in messages)
|
|
157
184
|
* @param port - Requested port
|
|
158
|
-
* @
|
|
159
|
-
*
|
|
185
|
+
* @param signal - Ends the request early when aborted
|
|
186
|
+
* @throws ChromeLaunchError: without an issue if `signal` aborts;
|
|
187
|
+
* PORT_IN_USE for a second listener; CHROME_LAUNCH_FAILED if no browser
|
|
188
|
+
* answers
|
|
160
189
|
*/
|
|
161
|
-
async function acceptUnannouncedChrome(logs, port) {
|
|
190
|
+
async function acceptUnannouncedChrome(logs, port, signal) {
|
|
162
191
|
const errLog = logs.files[0]?.file ?? 'chrome-err.log';
|
|
163
192
|
const missing = `Chrome did not print "DevTools listening on" to ${errLog}`;
|
|
164
|
-
|
|
193
|
+
const browserWsUrl = await fetchBrowserWsUrl(port, log, { signal });
|
|
194
|
+
throwIfLaunchAborted(signal);
|
|
195
|
+
if (!browserWsUrl) {
|
|
165
196
|
throw new ChromeLaunchError(`${missing}, and no browser answers on port ${port}`, {
|
|
166
197
|
issue: {
|
|
167
198
|
code: 'CHROME_LAUNCH_FAILED',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LaunchedChrome, Logger } from './types.js';
|
|
2
2
|
import type { Options as ChromeLaunchOptions } from 'chrome-launcher';
|
|
3
|
+
import { ChromeLaunchError } from './errors.js';
|
|
3
4
|
/**
|
|
4
5
|
* Options that control how Chrome is launched for CDP sessions.
|
|
5
6
|
* Extended to support chrome-launcher advanced features.
|
|
@@ -29,6 +30,11 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
|
|
|
29
30
|
chromePath?: string;
|
|
30
31
|
/** bdg session directory: recorded as a marker flag on the Chrome command line, and holds the default profile */
|
|
31
32
|
sessionDir?: string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Ends the launch when aborted (the session was stopped): bdg stops waiting
|
|
35
|
+
* and kills Chrome as soon as chrome-launcher has spawned it
|
|
36
|
+
*/
|
|
37
|
+
signal?: AbortSignal | undefined;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* Launch Chrome with remote debugging enabled using chrome-launcher.
|
|
@@ -42,7 +48,7 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
|
|
|
42
48
|
*
|
|
43
49
|
* @param options - Launch configuration options
|
|
44
50
|
* @returns LaunchedChrome instance with PID and kill method
|
|
45
|
-
* @throws ChromeLaunchError if Chrome fails to launch, process dies immediately,
|
|
51
|
+
* @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, CDP doesn't become available, or `options.signal` aborts
|
|
46
52
|
* @throws Error if user data directory cannot be created
|
|
47
53
|
*
|
|
48
54
|
* @remarks
|
|
@@ -50,6 +56,32 @@ export interface LaunchOptions extends Pick<ChromeLaunchOptions, 'logLevel' | 'c
|
|
|
50
56
|
* Uses chrome-launcher for cross-platform Chrome detection and launching.
|
|
51
57
|
*/
|
|
52
58
|
export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedChrome>;
|
|
59
|
+
/**
|
|
60
|
+
* State of the launch when chrome-launcher rejected.
|
|
61
|
+
*/
|
|
62
|
+
interface LaunchFailureContext {
|
|
63
|
+
/** Debugging port */
|
|
64
|
+
port: number;
|
|
65
|
+
/** Chrome's PID (0 if it was not spawned) */
|
|
66
|
+
pid: number;
|
|
67
|
+
/** Whether Chrome was still running when chrome-launcher rejected */
|
|
68
|
+
chromeAlive: boolean;
|
|
69
|
+
/** How long chrome-launcher waited for the port to open */
|
|
70
|
+
readyBudgetMs: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The error for a launch chrome-launcher rejected.
|
|
74
|
+
*
|
|
75
|
+
* A refused connection while Chrome is still running means Chrome did not
|
|
76
|
+
* open its port within the readiness budget (a slow start, e.g. the first
|
|
77
|
+
* one on a cold machine): the port was free right before the launch and
|
|
78
|
+
* nothing answers on it, so it is not a conflict, and Chrome did not crash.
|
|
79
|
+
*
|
|
80
|
+
* @param error - chrome-launcher's rejection
|
|
81
|
+
* @param context - Port, Chrome's PID and liveness, and the readiness budget
|
|
82
|
+
* @returns CHROME_PORT_NOT_OPENED for a slow start, else CHROME_LAUNCH_FAILED
|
|
83
|
+
*/
|
|
84
|
+
export declare function launchFailedError(error: unknown, { port, pid, chromeAlive, readyBudgetMs }: LaunchFailureContext): ChromeLaunchError;
|
|
53
85
|
/**
|
|
54
86
|
* Preferences for the launched profile: bdg's defaults (no translate prompt,
|
|
55
87
|
* no password manager or leak check, whose bubbles capture input) for
|
|
@@ -65,4 +97,5 @@ export declare function launchChrome(options?: LaunchOptions): Promise<LaunchedC
|
|
|
65
97
|
* @throws ChromeLaunchError if prefs file cannot be read or parsed, or prefs are not JSON
|
|
66
98
|
*/
|
|
67
99
|
export declare function resolveChromePrefs(options: LaunchOptions): Record<string, unknown>;
|
|
100
|
+
export {};
|
|
68
101
|
//# sourceMappingURL=launcher.d.ts.map
|