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
|
@@ -3,11 +3,13 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as chromeLauncher from 'chrome-launcher';
|
|
5
5
|
import { BDG_CHROME_PREFS, DEFAULT_CDP_PORT, CHROME_PROFILE_DIR, DEFAULT_CHROME_LOG_LEVEL, } from '../constants.js';
|
|
6
|
+
import { chromePortNotOpenedMessage } from '../ui/messages/chrome.js';
|
|
7
|
+
import { delay } from '../utils/async.js';
|
|
6
8
|
import { makeDirectory } from '../utils/directories.js';
|
|
7
9
|
import { getErrorMessage } from '../utils/errors.js';
|
|
8
10
|
import { filterDefined } from '../utils/objects.js';
|
|
9
11
|
import { isProcessAlive } from '../utils/process.js';
|
|
10
|
-
import { verifyLaunchedChrome } from './chromeIdentity.js';
|
|
12
|
+
import { launchAbortedError, throwIfLaunchAborted, verifyLaunchedChrome, } from './chromeIdentity.js';
|
|
11
13
|
import { ChromeLaunchError } from './errors.js';
|
|
12
14
|
import { resolveChromeBinary } from './launcher/binaryResolver.js';
|
|
13
15
|
import { buildChromeFlags } from './launcher/flagsBuilder.js';
|
|
@@ -27,6 +29,8 @@ import { markStartupLogs, watchStartupExit } from './startupExit.js';
|
|
|
27
29
|
const CHROME_READY_POLL_MS = 50;
|
|
28
30
|
/** Checks before giving up: 25 s in all, as with chrome-launcher's defaults */
|
|
29
31
|
const CHROME_READY_POLL_ATTEMPTS = 500;
|
|
32
|
+
/** How often an aborted launch checks whether chrome-launcher has spawned Chrome yet */
|
|
33
|
+
const SPAWN_POLL_MS = 10;
|
|
30
34
|
const defaultLogger = {
|
|
31
35
|
info: (msg) => console.error(msg),
|
|
32
36
|
debug: () => { }, // No-op by default
|
|
@@ -43,7 +47,7 @@ const defaultLogger = {
|
|
|
43
47
|
*
|
|
44
48
|
* @param options - Launch configuration options
|
|
45
49
|
* @returns LaunchedChrome instance with PID and kill method
|
|
46
|
-
* @throws ChromeLaunchError if Chrome fails to launch, process dies immediately,
|
|
50
|
+
* @throws ChromeLaunchError if Chrome fails to launch, process dies immediately, CDP doesn't become available, or `options.signal` aborts
|
|
47
51
|
* @throws Error if user data directory cannot be created
|
|
48
52
|
*
|
|
49
53
|
* @remarks
|
|
@@ -79,13 +83,14 @@ export async function launchChrome(options = {}) {
|
|
|
79
83
|
logger.debug(`User data directory: ${userDataDir}`);
|
|
80
84
|
applyProfilePreferences(userDataDir, options, logger);
|
|
81
85
|
const chromeOptions = buildChromeOptions({ ...options, port });
|
|
86
|
+
throwIfLaunchAborted(options.signal);
|
|
82
87
|
const launcher = new chromeLauncher.Launcher(chromeOptions);
|
|
83
88
|
const logs = markStartupLogs(userDataDir);
|
|
84
89
|
const startup = watchStartupExit(() => launcher.chromeProcess, logs, userDataDir);
|
|
85
90
|
try {
|
|
86
91
|
const launchStart = Date.now();
|
|
87
92
|
logger.info('Waiting for Chrome to be ready...');
|
|
88
|
-
await
|
|
93
|
+
await launchUnlessAborted(launcher, startup.exited, options.signal);
|
|
89
94
|
startup.stop();
|
|
90
95
|
const launchDurationMs = Date.now() - launchStart;
|
|
91
96
|
logger.info(`✓ Chrome ready (${launchDurationMs}ms)`);
|
|
@@ -101,7 +106,7 @@ export async function launchChrome(options = {}) {
|
|
|
101
106
|
},
|
|
102
107
|
});
|
|
103
108
|
}
|
|
104
|
-
await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid });
|
|
109
|
+
await verifyLaunchedChrome({ logs, port, pid: chromeProcessPid, signal: options.signal });
|
|
105
110
|
logger.info(`Chrome launched successfully (PID: ${chromeProcessPid}, ${launchDurationMs}ms)`);
|
|
106
111
|
return {
|
|
107
112
|
pid: chromeProcessPid,
|
|
@@ -117,20 +122,103 @@ export async function launchChrome(options = {}) {
|
|
|
117
122
|
}
|
|
118
123
|
catch (error) {
|
|
119
124
|
startup.stop();
|
|
125
|
+
const pid = launcher.chromeProcess?.pid ?? 0;
|
|
126
|
+
const chromeAlive = pid > 0 && isProcessAlive(pid);
|
|
120
127
|
launcher.kill();
|
|
121
128
|
launcher.destroyTmp();
|
|
122
129
|
if (error instanceof ChromeLaunchError) {
|
|
123
130
|
throw error;
|
|
124
131
|
}
|
|
125
|
-
throw
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
},
|
|
132
|
+
throw launchFailedError(error, {
|
|
133
|
+
port,
|
|
134
|
+
pid,
|
|
135
|
+
chromeAlive,
|
|
136
|
+
readyBudgetMs: readyBudgetMs(chromeOptions),
|
|
131
137
|
});
|
|
132
138
|
}
|
|
133
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The error for a launch chrome-launcher rejected.
|
|
142
|
+
*
|
|
143
|
+
* A refused connection while Chrome is still running means Chrome did not
|
|
144
|
+
* open its port within the readiness budget (a slow start, e.g. the first
|
|
145
|
+
* one on a cold machine): the port was free right before the launch and
|
|
146
|
+
* nothing answers on it, so it is not a conflict, and Chrome did not crash.
|
|
147
|
+
*
|
|
148
|
+
* @param error - chrome-launcher's rejection
|
|
149
|
+
* @param context - Port, Chrome's PID and liveness, and the readiness budget
|
|
150
|
+
* @returns CHROME_PORT_NOT_OPENED for a slow start, else CHROME_LAUNCH_FAILED
|
|
151
|
+
*/
|
|
152
|
+
export function launchFailedError(error, { port, pid, chromeAlive, readyBudgetMs }) {
|
|
153
|
+
const cause = error instanceof Error ? { cause: error } : {};
|
|
154
|
+
if (chromeAlive && isConnectionRefused(error)) {
|
|
155
|
+
return new ChromeLaunchError(chromePortNotOpenedMessage(pid, port, readyBudgetMs), {
|
|
156
|
+
...cause,
|
|
157
|
+
issue: { code: 'CHROME_PORT_NOT_OPENED', context: { port, pid, waitedMs: readyBudgetMs } },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return new ChromeLaunchError(`Failed to launch Chrome: ${getErrorMessage(error)}`, {
|
|
161
|
+
...cause,
|
|
162
|
+
issue: { code: 'CHROME_LAUNCH_FAILED', context: { port, reason: getErrorMessage(error) } },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Whether an error is a refused TCP connection.
|
|
167
|
+
*
|
|
168
|
+
* @param error - Error to check
|
|
169
|
+
* @returns True for ECONNREFUSED
|
|
170
|
+
*/
|
|
171
|
+
function isConnectionRefused(error) {
|
|
172
|
+
return error?.code === 'ECONNREFUSED';
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* How long chrome-launcher waits for Chrome's debugging port to open.
|
|
176
|
+
*
|
|
177
|
+
* @param chromeOptions - chrome-launcher options
|
|
178
|
+
* @returns Poll interval times the number of polls
|
|
179
|
+
*/
|
|
180
|
+
function readyBudgetMs(chromeOptions) {
|
|
181
|
+
return ((chromeOptions.connectionPollInterval ?? CHROME_READY_POLL_MS) *
|
|
182
|
+
(chromeOptions.maxConnectionRetries ?? CHROME_READY_POLL_ATTEMPTS));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run chrome-launcher's launch (spawn Chrome, wait for its port to open),
|
|
186
|
+
* ending early when Chrome exits or `signal` aborts.
|
|
187
|
+
*
|
|
188
|
+
* chrome-launcher cannot be cancelled: after an abort its port poller keeps
|
|
189
|
+
* running, bdg just stops waiting for it. An abort can arrive before
|
|
190
|
+
* chrome-launcher has spawned Chrome (it first checks whether the port
|
|
191
|
+
* answers), so this waits until Chrome is spawned or the launch has ended
|
|
192
|
+
* without one; the caller's kill then reaches that Chrome.
|
|
193
|
+
*
|
|
194
|
+
* @param launcher - chrome-launcher instance, not launched yet
|
|
195
|
+
* @param exited - Rejects when Chrome exits during startup
|
|
196
|
+
* @param signal - The launch's abort signal
|
|
197
|
+
* @throws ChromeLaunchError if `signal` aborts (before or during the launch)
|
|
198
|
+
* @throws Error from chrome-launcher or `exited`
|
|
199
|
+
*/
|
|
200
|
+
async function launchUnlessAborted(launcher, exited, signal) {
|
|
201
|
+
throwIfLaunchAborted(signal);
|
|
202
|
+
let settled = false;
|
|
203
|
+
const launching = launcher.launch().finally(() => {
|
|
204
|
+
settled = true;
|
|
205
|
+
});
|
|
206
|
+
let onAbort = () => { };
|
|
207
|
+
const aborted = new Promise((resolve) => {
|
|
208
|
+
onAbort = () => resolve('aborted');
|
|
209
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
if ((await Promise.race([launching, exited, aborted])) !== 'aborted')
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
signal?.removeEventListener('abort', onAbort);
|
|
217
|
+
}
|
|
218
|
+
while (!launcher.chromeProcess && !settled)
|
|
219
|
+
await delay(SPAWN_POLL_MS);
|
|
220
|
+
throw launchAbortedError();
|
|
221
|
+
}
|
|
134
222
|
/**
|
|
135
223
|
* Get the default persistent user-data-dir path.
|
|
136
224
|
*
|
|
@@ -18,9 +18,10 @@ import type { Protocol } from 'devtools-protocol/types/protocol.js';
|
|
|
18
18
|
* Extract parameter type from a CDP command.
|
|
19
19
|
*
|
|
20
20
|
* If command has no parameters, returns empty object type.
|
|
21
|
-
* If command has single parameter
|
|
21
|
+
* If command has a single parameter (required, or optional when all its
|
|
22
|
+
* fields are), returns its type.
|
|
22
23
|
*/
|
|
23
|
-
type CommandParams<T extends keyof ProtocolMapping.Commands> = ProtocolMapping.Commands[T]['paramsType'] extends [
|
|
24
|
+
type CommandParams<T extends keyof ProtocolMapping.Commands> = ProtocolMapping.Commands[T]['paramsType'] extends [] ? Record<string, never> : ProtocolMapping.Commands[T]['paramsType'] extends [(infer P)?] ? NonNullable<P> : never;
|
|
24
25
|
/**
|
|
25
26
|
* Extract return type from a CDP command.
|
|
26
27
|
*/
|
package/dist/constants.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export declare const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
|
|
|
86
86
|
*/
|
|
87
87
|
export declare const MAX_RESPONSE_SIZE: number;
|
|
88
88
|
/**
|
|
89
|
-
* Total size of the response bodies a session keeps (100MB)
|
|
89
|
+
* Total size of the request and response bodies a session keeps (100MB)
|
|
90
90
|
* Past this the oldest bodies are replaced by a placeholder; their requests stay
|
|
91
91
|
*/
|
|
92
92
|
export declare const MAX_TOTAL_BODY_BYTES: number;
|
package/dist/constants.js
CHANGED
|
@@ -118,7 +118,7 @@ export const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
|
|
|
118
118
|
*/
|
|
119
119
|
export const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
120
120
|
/**
|
|
121
|
-
* Total size of the response bodies a session keeps (100MB)
|
|
121
|
+
* Total size of the request and response bodies a session keeps (100MB)
|
|
122
122
|
* Past this the oldest bodies are replaced by a placeholder; their requests stay
|
|
123
123
|
*/
|
|
124
124
|
export const MAX_TOTAL_BODY_BYTES = 100 * 1024 * 1024;
|
|
@@ -41,7 +41,8 @@ export declare class SessionController {
|
|
|
41
41
|
stopActiveSession(): Promise<boolean>;
|
|
42
42
|
/**
|
|
43
43
|
* Error fields for a request that needs the session when there is none:
|
|
44
|
-
* still starting (85), or none at all
|
|
44
|
+
* still starting (85), or none at all (also while an abandoned start is
|
|
45
|
+
* torn down).
|
|
45
46
|
*
|
|
46
47
|
* @returns Error message, plus exit code and suggestion while starting
|
|
47
48
|
*/
|
|
@@ -78,15 +79,19 @@ export declare class SessionController {
|
|
|
78
79
|
* Execute a session command (dom_*, cdp_call, session_details, ...).
|
|
79
80
|
*
|
|
80
81
|
* @param request - Command request
|
|
82
|
+
* @param abandoned - Aborted when the requesting client disconnects
|
|
81
83
|
* @returns Command response, forwarding exit code and suggestion on failure
|
|
82
84
|
*/
|
|
83
|
-
command(request: ClientRequestUnion): Promise<unknown>;
|
|
85
|
+
command(request: ClientRequestUnion, abandoned?: AbortSignal): Promise<unknown>;
|
|
84
86
|
/**
|
|
85
87
|
* Start a session, or report the one already running.
|
|
86
88
|
*
|
|
87
|
-
* A start whose client disconnects (Ctrl-C
|
|
88
|
-
*
|
|
89
|
-
* learns that it exists.
|
|
89
|
+
* A start whose client disconnects (Ctrl-C, or a client killed without a
|
|
90
|
+
* clean disconnect) is abandoned: the session is stopped, whether it is
|
|
91
|
+
* still launching or has just started, since nobody learns that it exists.
|
|
92
|
+
* From then on the daemon reports itself shutting down (a new start gets
|
|
93
|
+
* the retryable `SESSION_SHUTTING_DOWN`, status shows the session ending),
|
|
94
|
+
* not starting, while Chrome is torn down.
|
|
90
95
|
*
|
|
91
96
|
* @param request - Start session request
|
|
92
97
|
* @param abandoned - Aborted when the requesting client disconnects
|
|
@@ -149,12 +149,13 @@ export class SessionController {
|
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
151
|
* Error fields for a request that needs the session when there is none:
|
|
152
|
-
* still starting (85), or none at all
|
|
152
|
+
* still starting (85), or none at all (also while an abandoned start is
|
|
153
|
+
* torn down).
|
|
153
154
|
*
|
|
154
155
|
* @returns Error message, plus exit code and suggestion while starting
|
|
155
156
|
*/
|
|
156
157
|
noSessionError() {
|
|
157
|
-
if (!this.launching)
|
|
158
|
+
if (!this.launching || this.closing)
|
|
158
159
|
return { error: noActiveSessionMessage() };
|
|
159
160
|
return {
|
|
160
161
|
error: STARTING_ERROR,
|
|
@@ -190,7 +191,7 @@ export class SessionController {
|
|
|
190
191
|
};
|
|
191
192
|
const base = { type: 'status_response', sessionId: request.sessionId };
|
|
192
193
|
if (!this.session || this.session.stopRequested()) {
|
|
193
|
-
if (this.launching) {
|
|
194
|
+
if (this.launching && !this.closing) {
|
|
194
195
|
data.starting = { url: this.launching.url, since: this.launching.since };
|
|
195
196
|
}
|
|
196
197
|
if (this.session || this.closing)
|
|
@@ -250,6 +251,7 @@ export class SessionController {
|
|
|
250
251
|
},
|
|
251
252
|
currentNavigationId: data.currentNavigationId,
|
|
252
253
|
...(data.pageCrashedAt !== undefined && { pageCrashedAt: data.pageCrashedAt }),
|
|
254
|
+
...(data.downloads && { downloads: data.downloads }),
|
|
253
255
|
partial: true,
|
|
254
256
|
},
|
|
255
257
|
},
|
|
@@ -282,9 +284,10 @@ export class SessionController {
|
|
|
282
284
|
* Execute a session command (dom_*, cdp_call, session_details, ...).
|
|
283
285
|
*
|
|
284
286
|
* @param request - Command request
|
|
287
|
+
* @param abandoned - Aborted when the requesting client disconnects
|
|
285
288
|
* @returns Command response, forwarding exit code and suggestion on failure
|
|
286
289
|
*/
|
|
287
|
-
async command(request) {
|
|
290
|
+
async command(request, abandoned) {
|
|
288
291
|
const name = request.type.slice(0, -'_request'.length);
|
|
289
292
|
const base = { type: `${name}_response`, sessionId: request.sessionId };
|
|
290
293
|
if (!this.session) {
|
|
@@ -293,7 +296,7 @@ export class SessionController {
|
|
|
293
296
|
const { sessionId: _sessionId, type: _type, ...params } = request;
|
|
294
297
|
try {
|
|
295
298
|
const execute = this.session.execute.bind(this.session);
|
|
296
|
-
const data = await withTimeout(execute(name, params), commandTimeoutMs(name, params), 'Command');
|
|
299
|
+
const data = await withTimeout(execute(name, params, abandoned), commandTimeoutMs(name, params), 'Command');
|
|
297
300
|
return { ...base, status: 'ok', data };
|
|
298
301
|
}
|
|
299
302
|
catch (error) {
|
|
@@ -303,9 +306,12 @@ export class SessionController {
|
|
|
303
306
|
/**
|
|
304
307
|
* Start a session, or report the one already running.
|
|
305
308
|
*
|
|
306
|
-
* A start whose client disconnects (Ctrl-C
|
|
307
|
-
*
|
|
308
|
-
* learns that it exists.
|
|
309
|
+
* A start whose client disconnects (Ctrl-C, or a client killed without a
|
|
310
|
+
* clean disconnect) is abandoned: the session is stopped, whether it is
|
|
311
|
+
* still launching or has just started, since nobody learns that it exists.
|
|
312
|
+
* From then on the daemon reports itself shutting down (a new start gets
|
|
313
|
+
* the retryable `SESSION_SHUTTING_DOWN`, status shows the session ending),
|
|
314
|
+
* not starting, while Chrome is torn down.
|
|
309
315
|
*
|
|
310
316
|
* @param request - Start session request
|
|
311
317
|
* @param abandoned - Aborted when the requesting client disconnects
|
|
@@ -330,6 +336,7 @@ export class SessionController {
|
|
|
330
336
|
this.launching = launching;
|
|
331
337
|
const stopAbandoned = () => {
|
|
332
338
|
log.info('Client disconnected during start; stopping the session');
|
|
339
|
+
this.closing = true;
|
|
333
340
|
void launching.session?.stop('normal');
|
|
334
341
|
};
|
|
335
342
|
abandoned?.addEventListener('abort', stopAbandoned, { once: true });
|
package/dist/daemon/ipcServer.js
CHANGED
|
@@ -196,7 +196,7 @@ export class IPCServer {
|
|
|
196
196
|
*/
|
|
197
197
|
async route(message, disconnected) {
|
|
198
198
|
if (isCommandRequest(message.type)) {
|
|
199
|
-
return this.controller.command(message);
|
|
199
|
+
return this.controller.command(message, disconnected);
|
|
200
200
|
}
|
|
201
201
|
switch (message.type) {
|
|
202
202
|
case 'handshake_request':
|
|
@@ -11,6 +11,11 @@ export interface SpawnedDaemon {
|
|
|
11
11
|
pid: number | undefined;
|
|
12
12
|
/** Whether it has exited (from the child process's exit event) */
|
|
13
13
|
hasExited: () => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Tell it to shut down (SIGTERM, unless it has exited): it ends what it
|
|
16
|
+
* hosts and exits. For a start interrupted before its request was sent.
|
|
17
|
+
*/
|
|
18
|
+
stop: () => void;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Ensure a daemon is running, spawning one if needed.
|
package/dist/daemon/launcher.js
CHANGED
|
@@ -58,7 +58,14 @@ export async function launchDaemon() {
|
|
|
58
58
|
});
|
|
59
59
|
daemon.unref();
|
|
60
60
|
await waitForDaemonReady(() => exited);
|
|
61
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
pid: daemon.pid,
|
|
63
|
+
hasExited: () => exited,
|
|
64
|
+
stop: () => {
|
|
65
|
+
if (!exited)
|
|
66
|
+
daemon.kill('SIGTERM');
|
|
67
|
+
},
|
|
68
|
+
};
|
|
62
69
|
}
|
|
63
70
|
/** How long a daemon that lost its socket gets to end its session before it is killed */
|
|
64
71
|
const UNREACHABLE_DAEMON_EXIT_MS = 8000;
|
|
@@ -39,6 +39,7 @@ export declare class Session {
|
|
|
39
39
|
private readonly onEnded;
|
|
40
40
|
private readonly store;
|
|
41
41
|
private readonly registry;
|
|
42
|
+
private readonly captures;
|
|
42
43
|
private readonly notify;
|
|
43
44
|
private chrome;
|
|
44
45
|
private cdp;
|
|
@@ -85,12 +86,15 @@ export declare class Session {
|
|
|
85
86
|
* {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
|
|
86
87
|
* waiting for a page that cannot answer. Commands that may change the page
|
|
87
88
|
* drop `dom inspect`'s kept matched rules ({@link withMatchedStylesReset}).
|
|
89
|
+
* Page commands run once a screenshot running before them has put the
|
|
90
|
+
* page's emulation back ({@link CaptureGate}).
|
|
88
91
|
*
|
|
89
92
|
* @param name - Command name
|
|
90
93
|
* @param params - Command parameters
|
|
94
|
+
* @param abandoned - Aborted when the requesting client disconnects
|
|
91
95
|
* @returns Command result
|
|
92
96
|
*/
|
|
93
|
-
execute<K extends CommandName>(name: K, params: CommandSchemas[K]['requestSchema']): Promise<CommandSchemas[K]['responseSchema']>;
|
|
97
|
+
execute<K extends CommandName>(name: K, params: CommandSchemas[K]['requestSchema'], abandoned?: AbortSignal): Promise<CommandSchemas[K]['responseSchema']>;
|
|
94
98
|
/**
|
|
95
99
|
* Summary of this session for start responses.
|
|
96
100
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { ChromeLaunchError } from '../../connection/errors.js';
|
|
9
9
|
import { TelemetryStore } from './TelemetryStore.js';
|
|
10
|
+
import { CaptureGate, TELEMETRY_READS } from './captureGate.js';
|
|
10
11
|
import { connectCDP, navigateToTarget } from './cdpSetup.js';
|
|
11
12
|
import { externalChromePort, findPageTarget, setupChromeConnection, } from './chromeConnection.js';
|
|
12
13
|
import { startTelemetryCollectors } from './collectors.js';
|
|
@@ -40,11 +41,7 @@ const PORT_ATTEMPTS = 3;
|
|
|
40
41
|
* send raw CDP. Every other command needs the page and fails at once.
|
|
41
42
|
*/
|
|
42
43
|
const RUN_ON_CRASHED_PAGE = new Set([
|
|
43
|
-
|
|
44
|
-
'session_details',
|
|
45
|
-
'session_status',
|
|
46
|
-
'session_har_data',
|
|
47
|
-
'session_network_headers',
|
|
44
|
+
...TELEMETRY_READS,
|
|
48
45
|
'page_navigate',
|
|
49
46
|
'cdp_call',
|
|
50
47
|
]);
|
|
@@ -83,6 +80,7 @@ export class Session {
|
|
|
83
80
|
get: () => filterDefined({ viewport: this.config.viewport, colorScheme: this.config.colorScheme }),
|
|
84
81
|
set: (emulation) => this.setEmulation(emulation),
|
|
85
82
|
});
|
|
83
|
+
captures = new CaptureGate();
|
|
86
84
|
notify = (notice) => log.info(formatChromeNotice(notice));
|
|
87
85
|
chrome = null;
|
|
88
86
|
cdp = null;
|
|
@@ -155,12 +153,15 @@ export class Session {
|
|
|
155
153
|
* {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
|
|
156
154
|
* waiting for a page that cannot answer. Commands that may change the page
|
|
157
155
|
* drop `dom inspect`'s kept matched rules ({@link withMatchedStylesReset}).
|
|
156
|
+
* Page commands run once a screenshot running before them has put the
|
|
157
|
+
* page's emulation back ({@link CaptureGate}).
|
|
158
158
|
*
|
|
159
159
|
* @param name - Command name
|
|
160
160
|
* @param params - Command parameters
|
|
161
|
+
* @param abandoned - Aborted when the requesting client disconnects
|
|
161
162
|
* @returns Command result
|
|
162
163
|
*/
|
|
163
|
-
execute(name, params) {
|
|
164
|
+
execute(name, params, abandoned) {
|
|
164
165
|
if (!this.cdp || !this.started || this.stopping) {
|
|
165
166
|
return Promise.reject(new Error('No active session'));
|
|
166
167
|
}
|
|
@@ -176,7 +177,7 @@ export class Session {
|
|
|
176
177
|
}
|
|
177
178
|
const handler = this.registry[name];
|
|
178
179
|
const cdp = this.cdp;
|
|
179
|
-
return withMatchedStylesReset(cdp, name, () => handler(cdp, params));
|
|
180
|
+
return this.captures.run(name, () => withMatchedStylesReset(cdp, name, () => handler(cdp, params, abandoned)));
|
|
180
181
|
}
|
|
181
182
|
/**
|
|
182
183
|
* Summary of this session for start responses.
|
|
@@ -315,7 +316,7 @@ export class Session {
|
|
|
315
316
|
this.config = { ...this.config, port: await getSessionPort(explicitPort) };
|
|
316
317
|
this.throwIfStopping();
|
|
317
318
|
try {
|
|
318
|
-
this.chrome = await setupChromeConnection(this.config, this.store, log, this.notify);
|
|
319
|
+
this.chrome = await setupChromeConnection(this.config, this.store, log, this.notify, this.launchAbort.signal);
|
|
319
320
|
return;
|
|
320
321
|
}
|
|
321
322
|
catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DialogInfo } from '../../ipc/protocol/domTypes.js';
|
|
2
|
+
import type { TrackedDownload } from '../../telemetry/downloads.js';
|
|
2
3
|
import type { NavigationEvent } from '../../telemetry/navigation.js';
|
|
3
4
|
import type { PendingRequest } from '../../telemetry/network.js';
|
|
4
5
|
import type { NetworkEvictions } from '../../telemetry/networkRetention.js';
|
|
@@ -23,6 +24,10 @@ export declare class TelemetryStore {
|
|
|
23
24
|
readonly websocketConnections: WebSocketConnection[];
|
|
24
25
|
/** JavaScript dialogs accepted during the session */
|
|
25
26
|
readonly dialogs: DialogInfo[];
|
|
27
|
+
/** Downloads that began during the session, oldest first, updated as they progress */
|
|
28
|
+
readonly downloads: TrackedDownload[];
|
|
29
|
+
/** Set while downloads do not go where bdg meant them to (refused, or not redirected) */
|
|
30
|
+
downloadsWarning: string | undefined;
|
|
26
31
|
activeTelemetry: TelemetryType[];
|
|
27
32
|
/** When the page's renderer crashed (epoch ms); undefined while the page is alive */
|
|
28
33
|
pageCrashedAt: number | undefined;
|
|
@@ -20,6 +20,10 @@ export class TelemetryStore {
|
|
|
20
20
|
websocketConnections = [];
|
|
21
21
|
/** JavaScript dialogs accepted during the session */
|
|
22
22
|
dialogs = [];
|
|
23
|
+
/** Downloads that began during the session, oldest first, updated as they progress */
|
|
24
|
+
downloads = [];
|
|
25
|
+
/** Set while downloads do not go where bdg meant them to (refused, or not redirected) */
|
|
26
|
+
downloadsWarning = undefined;
|
|
23
27
|
activeTelemetry = [];
|
|
24
28
|
/** When the page's renderer crashed (epoch ms); undefined while the page is alive */
|
|
25
29
|
pageCrashedAt;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A screenshot changes the page's emulation for its capture (pixel ratio,
|
|
3
|
+
* touch, viewport, scrollbars) and puts it back once the capture ended. Its
|
|
4
|
+
* client may be gone long before that: interrupted (Ctrl-C), killed, or timed
|
|
5
|
+
* out by the daemon's command timeout. Page commands therefore run only once
|
|
6
|
+
* the captures before them are done, restore included, so no command, from
|
|
7
|
+
* any client, sees the capture's emulation. Commands that read only the
|
|
8
|
+
* telemetry run at once. The wait counts toward the command's own timeout
|
|
9
|
+
* (30 s), like any time the command takes.
|
|
10
|
+
*/
|
|
11
|
+
import type { CommandName } from '../../ipc/index.js';
|
|
12
|
+
/** Commands that read only what the session collected, never the page */
|
|
13
|
+
export declare const TELEMETRY_READS: readonly CommandName[];
|
|
14
|
+
/**
|
|
15
|
+
* How long a page command waits for a capture before it runs anyway. A
|
|
16
|
+
* capture of a very tall page takes a few seconds; one that never ends (CDP
|
|
17
|
+
* calls have no timeout) must not stall the session. A capture waits for the
|
|
18
|
+
* one before it without this limit (bounded by its command timeout): run
|
|
19
|
+
* during it, it would record that capture's emulation as the page's own and
|
|
20
|
+
* put that back.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CAPTURE_WAIT_MS = 15000;
|
|
23
|
+
/**
|
|
24
|
+
* Orders page commands after the captures before them.
|
|
25
|
+
*/
|
|
26
|
+
export declare class CaptureGate {
|
|
27
|
+
private readonly waitMs;
|
|
28
|
+
private capture;
|
|
29
|
+
/**
|
|
30
|
+
* @param waitMs - How long a command waits for a capture at most
|
|
31
|
+
*/
|
|
32
|
+
constructor(waitMs?: number);
|
|
33
|
+
/**
|
|
34
|
+
* Run a command: at once when it reads only the telemetry, else once the
|
|
35
|
+
* capture running before it has ended (or, unless it is a capture itself,
|
|
36
|
+
* {@link waitMs} passed). A
|
|
37
|
+
* capture is waited for by the commands after it until it ends, however it
|
|
38
|
+
* ends.
|
|
39
|
+
*
|
|
40
|
+
* @param name - Command name
|
|
41
|
+
* @param command - The command
|
|
42
|
+
* @returns Its result
|
|
43
|
+
*/
|
|
44
|
+
run<T>(name: CommandName, command: () => Promise<T>): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Wait for the running capture, if any: until it ends for a capture, else
|
|
47
|
+
* at most {@link waitMs}.
|
|
48
|
+
*
|
|
49
|
+
* @param isCapture - Whether the waiting command is a capture
|
|
50
|
+
*/
|
|
51
|
+
private captureEnded;
|
|
52
|
+
/**
|
|
53
|
+
* Make a capture the one later commands wait for.
|
|
54
|
+
*
|
|
55
|
+
* @param result - The capture's result
|
|
56
|
+
*/
|
|
57
|
+
private track;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=captureGate.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A screenshot changes the page's emulation for its capture (pixel ratio,
|
|
3
|
+
* touch, viewport, scrollbars) and puts it back once the capture ended. Its
|
|
4
|
+
* client may be gone long before that: interrupted (Ctrl-C), killed, or timed
|
|
5
|
+
* out by the daemon's command timeout. Page commands therefore run only once
|
|
6
|
+
* the captures before them are done, restore included, so no command, from
|
|
7
|
+
* any client, sees the capture's emulation. Commands that read only the
|
|
8
|
+
* telemetry run at once. The wait counts toward the command's own timeout
|
|
9
|
+
* (30 s), like any time the command takes.
|
|
10
|
+
*/
|
|
11
|
+
import { settledWithin } from '../../runtime/dom/evalHelpers.js';
|
|
12
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
13
|
+
const log = createLogger('session');
|
|
14
|
+
/** Commands that read only what the session collected, never the page */
|
|
15
|
+
export const TELEMETRY_READS = [
|
|
16
|
+
'session_peek',
|
|
17
|
+
'session_details',
|
|
18
|
+
'session_status',
|
|
19
|
+
'session_har_data',
|
|
20
|
+
'session_network_headers',
|
|
21
|
+
];
|
|
22
|
+
const IGNORES_CAPTURE = new Set(TELEMETRY_READS);
|
|
23
|
+
/**
|
|
24
|
+
* How long a page command waits for a capture before it runs anyway. A
|
|
25
|
+
* capture of a very tall page takes a few seconds; one that never ends (CDP
|
|
26
|
+
* calls have no timeout) must not stall the session. A capture waits for the
|
|
27
|
+
* one before it without this limit (bounded by its command timeout): run
|
|
28
|
+
* during it, it would record that capture's emulation as the page's own and
|
|
29
|
+
* put that back.
|
|
30
|
+
*/
|
|
31
|
+
export const CAPTURE_WAIT_MS = 15_000;
|
|
32
|
+
/** Command that changes the page's emulation until it ends */
|
|
33
|
+
const CAPTURE_COMMAND = 'dom_screenshot';
|
|
34
|
+
/**
|
|
35
|
+
* Orders page commands after the captures before them.
|
|
36
|
+
*/
|
|
37
|
+
export class CaptureGate {
|
|
38
|
+
waitMs;
|
|
39
|
+
capture;
|
|
40
|
+
/**
|
|
41
|
+
* @param waitMs - How long a command waits for a capture at most
|
|
42
|
+
*/
|
|
43
|
+
constructor(waitMs = CAPTURE_WAIT_MS) {
|
|
44
|
+
this.waitMs = waitMs;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run a command: at once when it reads only the telemetry, else once the
|
|
48
|
+
* capture running before it has ended (or, unless it is a capture itself,
|
|
49
|
+
* {@link waitMs} passed). A
|
|
50
|
+
* capture is waited for by the commands after it until it ends, however it
|
|
51
|
+
* ends.
|
|
52
|
+
*
|
|
53
|
+
* @param name - Command name
|
|
54
|
+
* @param command - The command
|
|
55
|
+
* @returns Its result
|
|
56
|
+
*/
|
|
57
|
+
run(name, command) {
|
|
58
|
+
if (IGNORES_CAPTURE.has(name))
|
|
59
|
+
return command();
|
|
60
|
+
const isCapture = name === CAPTURE_COMMAND;
|
|
61
|
+
const result = this.captureEnded(isCapture).then(command);
|
|
62
|
+
if (isCapture)
|
|
63
|
+
this.track(result);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wait for the running capture, if any: until it ends for a capture, else
|
|
68
|
+
* at most {@link waitMs}.
|
|
69
|
+
*
|
|
70
|
+
* @param isCapture - Whether the waiting command is a capture
|
|
71
|
+
*/
|
|
72
|
+
async captureEnded(isCapture) {
|
|
73
|
+
const capture = this.capture;
|
|
74
|
+
if (!capture)
|
|
75
|
+
return;
|
|
76
|
+
if (isCapture)
|
|
77
|
+
return capture;
|
|
78
|
+
const ended = await settledWithin(capture, this.waitMs);
|
|
79
|
+
if (!ended.settled)
|
|
80
|
+
log.info(`Capture still running after ${this.waitMs} ms; not waiting`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Make a capture the one later commands wait for.
|
|
84
|
+
*
|
|
85
|
+
* @param result - The capture's result
|
|
86
|
+
*/
|
|
87
|
+
track(result) {
|
|
88
|
+
const ended = result.then(() => undefined, () => undefined);
|
|
89
|
+
this.capture = ended;
|
|
90
|
+
void ended.then(() => {
|
|
91
|
+
if (this.capture === ended)
|
|
92
|
+
this.capture = undefined;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=captureGate.js.map
|
|
@@ -12,9 +12,24 @@ import type { Logger } from '../../ui/logging/index.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* Setup Chrome connection - either launch new instance or connect to existing.
|
|
14
14
|
*
|
|
15
|
+
* @param config - Session configuration
|
|
16
|
+
* @param telemetryStore - Store receiving the target info
|
|
17
|
+
* @param log - Logger
|
|
18
|
+
* @param notify - Receives notices about an external Chrome
|
|
19
|
+
* @param signal - Ends a launch at once when aborted (the session was stopped)
|
|
15
20
|
* @returns Launched Chrome instance (null if connecting to external Chrome)
|
|
16
21
|
*/
|
|
17
|
-
export declare function setupChromeConnection(config: SessionConfig, telemetryStore: TelemetryStore, log: Logger, notify: NoticeSink<ChromeNoticeCode
|
|
22
|
+
export declare function setupChromeConnection(config: SessionConfig, telemetryStore: TelemetryStore, log: Logger, notify: NoticeSink<ChromeNoticeCode>, signal?: AbortSignal): Promise<LaunchedChrome | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Browser-level DevTools WebSocket URL of the session's Chrome: a launched
|
|
25
|
+
* Chrome's, or for `--chrome-ws-url` the URL itself when it is browser-level,
|
|
26
|
+
* else the one Chrome reports, reached the way the user's URL is.
|
|
27
|
+
*
|
|
28
|
+
* @param config - Session configuration (port resolved)
|
|
29
|
+
* @param log - Logger
|
|
30
|
+
* @returns The URL, or null when Chrome does not answer `/json/version`
|
|
31
|
+
*/
|
|
32
|
+
export declare function browserWebSocketUrl(config: SessionConfig, log: Logger): Promise<string | null>;
|
|
18
33
|
/**
|
|
19
34
|
* Debugging port of an external Chrome, taken from its WebSocket URL.
|
|
20
35
|
*
|