browser-debugger-cli 0.14.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.
Files changed (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -11,11 +11,18 @@ 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.
17
22
  *
18
23
  * @returns The daemon it spawned, or undefined when one was already running
24
+ * @throws SessionDirError if the session directory is unusable or untrusted
25
+ * (checked first, so a socket planted there is never taken for a daemon)
19
26
  * @throws DaemonStartupError if the daemon script is missing or the daemon
20
27
  * does not accept connections in time
21
28
  */
@@ -24,9 +31,21 @@ export declare function launchDaemon(): Promise<SpawnedDaemon | undefined>;
24
31
  * Check that the session directory can hold the daemon's files before
25
32
  * spawning it (otherwise the daemon dies and only its log says why).
26
33
  *
27
- * @throws SessionDirError (103) for a file or a path that cannot hold a
28
- * directory (a pseudo-filesystem like `/proc`), (81) for a too-long path
29
- * like a named session's, (82) when not writable
34
+ * @throws SessionDirError (103) for a file, a path that cannot hold a
35
+ * directory (a pseudo-filesystem like `/proc`) or an untrusted directory
36
+ * (see {@link secureSessionDir}, which also tightens the user's own 0755
37
+ * directories to 0700), (81) for a too-long path like a named session's,
38
+ * (82) when not writable
30
39
  */
31
40
  export declare function assertUsableSessionDir(): void;
41
+ /**
42
+ * Open the daemon log for the daemon's output, creating it 0600. A symlink in
43
+ * its place is refused, not followed: it would append the log to the file it
44
+ * points to.
45
+ *
46
+ * @param logPath - Daemon log path
47
+ * @returns File descriptor
48
+ * @throws SessionDirError (103) when it cannot be opened (`ELOOP` for a symlink)
49
+ */
50
+ export declare function openDaemonLog(logPath: string): number;
32
51
  //# sourceMappingURL=launcher.d.ts.map
@@ -10,10 +10,10 @@ import { spawn } from 'child_process';
10
10
  import fs from 'fs';
11
11
  import { join } from 'path';
12
12
  import { DaemonStartupError, SessionDirError } from './errors.js';
13
- import { sessionDirIsFileError, sessionDirNotWritableError, socketPathTooLongError, } from '../errors/messages.js';
13
+ import { daemonLogNotOpenedError, sessionDirIsFileError, sessionDirNotWritableError, socketPathTooLongError, untrustedSessionDirError, } from '../errors/messages.js';
14
14
  import { killSessionChromes, readLiveDaemonPid } from '../session/cleanup/staleSession.js';
15
15
  import { isDaemonAlive, isDaemonSocketGone } from '../session/daemonSocket.js';
16
- import { MAX_DAEMON_SOCKET_PATH_BYTES, ensureSessionDir, getSessionDir, getSessionFilePath, } from '../session/paths.js';
16
+ import { MAX_DAEMON_SOCKET_PATH_BYTES, ensureSessionDir, getSessionDir, getSessionFilePath, secureSessionDir, } from '../session/paths.js';
17
17
  import { createLogger } from '../ui/logging/index.js';
18
18
  import { delay } from '../utils/async.js';
19
19
  import { directoryProblem } from '../utils/directories.js';
@@ -28,10 +28,13 @@ const DAEMON_READY_POLL_MS = 20;
28
28
  * Ensure a daemon is running, spawning one if needed.
29
29
  *
30
30
  * @returns The daemon it spawned, or undefined when one was already running
31
+ * @throws SessionDirError if the session directory is unusable or untrusted
32
+ * (checked first, so a socket planted there is never taken for a daemon)
31
33
  * @throws DaemonStartupError if the daemon script is missing or the daemon
32
34
  * does not accept connections in time
33
35
  */
34
36
  export async function launchDaemon() {
37
+ assertUsableSessionDir();
35
38
  if (await isDaemonAlive()) {
36
39
  log.debug('Daemon already running');
37
40
  return undefined;
@@ -39,11 +42,10 @@ export async function launchDaemon() {
39
42
  if (!fs.existsSync(DAEMON_SCRIPT_PATH)) {
40
43
  throw new DaemonStartupError(`Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`, 'DAEMON_SCRIPT_NOT_FOUND');
41
44
  }
42
- assertUsableSessionDir();
43
45
  await stopUnreachableDaemon();
44
46
  const logPath = join(getSessionDir(), 'daemon.log');
45
47
  rotateLog(logPath);
46
- const logFd = fs.openSync(logPath, 'a');
48
+ const logFd = openDaemonLog(logPath);
47
49
  log.debug(`Starting daemon: ${DAEMON_SCRIPT_PATH}`);
48
50
  const daemon = spawn(process.execPath, [DAEMON_SCRIPT_PATH], {
49
51
  detached: true,
@@ -56,7 +58,14 @@ export async function launchDaemon() {
56
58
  });
57
59
  daemon.unref();
58
60
  await waitForDaemonReady(() => exited);
59
- return { pid: daemon.pid, hasExited: () => exited };
61
+ return {
62
+ pid: daemon.pid,
63
+ hasExited: () => exited,
64
+ stop: () => {
65
+ if (!exited)
66
+ daemon.kill('SIGTERM');
67
+ },
68
+ };
60
69
  }
61
70
  /** How long a daemon that lost its socket gets to end its session before it is killed */
62
71
  const UNREACHABLE_DAEMON_EXIT_MS = 8000;
@@ -97,9 +106,11 @@ async function stopUnreachableDaemon() {
97
106
  * Check that the session directory can hold the daemon's files before
98
107
  * spawning it (otherwise the daemon dies and only its log says why).
99
108
  *
100
- * @throws SessionDirError (103) for a file or a path that cannot hold a
101
- * directory (a pseudo-filesystem like `/proc`), (81) for a too-long path
102
- * like a named session's, (82) when not writable
109
+ * @throws SessionDirError (103) for a file, a path that cannot hold a
110
+ * directory (a pseudo-filesystem like `/proc`) or an untrusted directory
111
+ * (see {@link secureSessionDir}, which also tightens the user's own 0755
112
+ * directories to 0700), (81) for a too-long path like a named session's,
113
+ * (82) when not writable
103
114
  */
104
115
  export function assertUsableSessionDir() {
105
116
  const dir = getSessionDir();
@@ -116,6 +127,9 @@ export function assertUsableSessionDir() {
116
127
  if (problem) {
117
128
  fail(sessionDirNotWritableError(dir, problem.reason), problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR);
118
129
  }
130
+ const untrusted = secureSessionDir(dir);
131
+ if (untrusted)
132
+ fail(untrustedSessionDirError(untrusted));
119
133
  try {
120
134
  ensureSessionDir();
121
135
  fs.accessSync(dir, fs.constants.W_OK);
@@ -126,6 +140,29 @@ export function assertUsableSessionDir() {
126
140
  fail(sessionDirNotWritableError(dir, code ?? getErrorMessage(error)), denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR);
127
141
  }
128
142
  }
143
+ /** Flags opening the daemon log for appending, refusing (not following) a symlink */
144
+ const DAEMON_LOG_FLAGS = fs.constants.O_WRONLY |
145
+ fs.constants.O_APPEND |
146
+ fs.constants.O_CREAT |
147
+ (fs.constants.O_NOFOLLOW ?? 0);
148
+ /**
149
+ * Open the daemon log for the daemon's output, creating it 0600. A symlink in
150
+ * its place is refused, not followed: it would append the log to the file it
151
+ * points to.
152
+ *
153
+ * @param logPath - Daemon log path
154
+ * @returns File descriptor
155
+ * @throws SessionDirError (103) when it cannot be opened (`ELOOP` for a symlink)
156
+ */
157
+ export function openDaemonLog(logPath) {
158
+ try {
159
+ return fs.openSync(logPath, DAEMON_LOG_FLAGS, 0o600);
160
+ }
161
+ catch (error) {
162
+ const err = daemonLogNotOpenedError(logPath, error.code ?? getErrorMessage(error));
163
+ throw new SessionDirError(err.message, err.suggestion);
164
+ }
165
+ }
129
166
  /** Size above which the daemon log is rotated when a daemon starts */
130
167
  const MAX_LOG_BYTES = 5 * 1024 * 1024;
131
168
  /**
@@ -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
- 'session_peek',
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>): Promise<LaunchedChrome | null>;
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
  *
@@ -14,23 +14,47 @@ import { writeChromePid } from '../../session/chrome.js';
14
14
  import { findConflictingOwner } from '../../session/chromeOwners.js';
15
15
  import { getSessionDir } from '../../session/paths.js';
16
16
  import { EXIT_CODES } from '../../utils/exitCodes.js';
17
- import { createPageTarget, fetchCDPTargets, probeDevToolsEndpoint } from '../../utils/http.js';
17
+ import { createPageTarget, fetchBrowserWsUrl, fetchCDPTargets, probeDevToolsEndpoint, } from '../../utils/http.js';
18
18
  import { filterDefined } from '../../utils/objects.js';
19
19
  /**
20
20
  * Setup Chrome connection - either launch new instance or connect to existing.
21
21
  *
22
+ * @param config - Session configuration
23
+ * @param telemetryStore - Store receiving the target info
24
+ * @param log - Logger
25
+ * @param notify - Receives notices about an external Chrome
26
+ * @param signal - Ends a launch at once when aborted (the session was stopped)
22
27
  * @returns Launched Chrome instance (null if connecting to external Chrome)
23
28
  */
24
- export async function setupChromeConnection(config, telemetryStore, log, notify) {
29
+ export async function setupChromeConnection(config, telemetryStore, log, notify, signal) {
25
30
  if (config.chromeWsUrl) {
26
31
  return setupExternalChrome(config, telemetryStore, log, notify);
27
32
  }
28
33
  else {
29
- return setupLaunchedChrome(config, log);
34
+ return setupLaunchedChrome(config, log, signal);
30
35
  }
31
36
  }
32
37
  /** Path of browser-level DevTools WebSocket URLs (`/devtools/browser/<uuid>`) */
33
38
  const BROWSER_WS_PATH = '/devtools/browser/';
39
+ /**
40
+ * Browser-level DevTools WebSocket URL of the session's Chrome: a launched
41
+ * Chrome's, or for `--chrome-ws-url` the URL itself when it is browser-level,
42
+ * else the one Chrome reports, reached the way the user's URL is.
43
+ *
44
+ * @param config - Session configuration (port resolved)
45
+ * @param log - Logger
46
+ * @returns The URL, or null when Chrome does not answer `/json/version`
47
+ */
48
+ export async function browserWebSocketUrl(config, log) {
49
+ if (!config.chromeWsUrl)
50
+ return fetchBrowserWsUrl(config.port, log);
51
+ const { hostname, pathname, protocol, port } = new URL(config.chromeWsUrl);
52
+ if (pathname.startsWith(BROWSER_WS_PATH))
53
+ return config.chromeWsUrl;
54
+ const http = { host: hostname, secure: protocol === 'wss:' };
55
+ const reported = await fetchBrowserWsUrl(config.port, log, http);
56
+ return reported && withEndpoint(reported, protocol, hostname, port);
57
+ }
34
58
  /**
35
59
  * Debugging port of an external Chrome, taken from its WebSocket URL.
36
60
  *
@@ -162,12 +186,18 @@ export function windowSizeFlags(config) {
162
186
  }
163
187
  /**
164
188
  * Launch a new Chrome instance and record its PID for crash cleanup.
189
+ *
190
+ * @param config - Session configuration
191
+ * @param log - Logger
192
+ * @param signal - Ends the launch at once when aborted
193
+ * @returns Launched Chrome
165
194
  */
166
- async function setupLaunchedChrome(config, log) {
195
+ async function setupLaunchedChrome(config, log, signal) {
167
196
  const chrome = await launchChrome({
168
197
  port: config.port,
169
198
  logger: log,
170
199
  sessionDir: getSessionDir(),
200
+ signal,
171
201
  ...filterDefined({
172
202
  userDataDir: config.userDataDir,
173
203
  headless: config.headless,
@@ -4,5 +4,20 @@ import type { SessionConfig } from './types.js';
4
4
  import type { CDPConnection } from '../../connection/cdp.js';
5
5
  import type { CleanupFunction } from '../../types.js';
6
6
  import type { Logger } from '../../ui/logging/index.js';
7
+ /**
8
+ * Start the session's collectors (telemetry plugins) in order.
9
+ *
10
+ * When one fails to start, the ones already started are cleaned up (their
11
+ * failures logged) before the error is rethrown, so nothing they opened (a
12
+ * browser-level connection, event handlers) outlives the failed start.
13
+ *
14
+ * @param cdp - Page connection
15
+ * @param config - Session configuration
16
+ * @param store - Telemetry store
17
+ * @param logger - Logger
18
+ * @param plugins - Plugins to start (default: the registered ones)
19
+ * @returns Cleanup functions of the started collectors
20
+ * @throws The error of the collector that failed to start
21
+ */
7
22
  export declare function startTelemetryCollectors(cdp: CDPConnection, config: SessionConfig, store: TelemetryStore, logger: Logger, plugins?: TelemetryPlugin[]): Promise<CleanupFunction[]>;
8
23
  //# sourceMappingURL=collectors.d.ts.map
@@ -1,6 +1,22 @@
1
1
  import { sessionActivatingCollector, sessionCollectorsActivated } from '../messages.js';
2
+ import { getErrorMessage } from '../../utils/errors.js';
2
3
  import { getRegisteredTelemetryPlugins, shouldActivatePlugin } from './plugins.js';
3
4
  const DEFAULT_TELEMETRY = ['network', 'console', 'dom'];
5
+ /**
6
+ * Start the session's collectors (telemetry plugins) in order.
7
+ *
8
+ * When one fails to start, the ones already started are cleaned up (their
9
+ * failures logged) before the error is rethrown, so nothing they opened (a
10
+ * browser-level connection, event handlers) outlives the failed start.
11
+ *
12
+ * @param cdp - Page connection
13
+ * @param config - Session configuration
14
+ * @param store - Telemetry store
15
+ * @param logger - Logger
16
+ * @param plugins - Plugins to start (default: the registered ones)
17
+ * @returns Cleanup functions of the started collectors
18
+ * @throws The error of the collector that failed to start
19
+ */
4
20
  export async function startTelemetryCollectors(cdp, config, store, logger, plugins) {
5
21
  const cleanupFunctions = [];
6
22
  store.activeTelemetry = config.telemetry ?? DEFAULT_TELEMETRY;
@@ -10,10 +26,31 @@ export async function startTelemetryCollectors(cdp, config, store, logger, plugi
10
26
  continue;
11
27
  }
12
28
  logger.debug(sessionActivatingCollector(plugin.name));
13
- const cleanup = await plugin.start({ cdp, config, store, logger });
14
- cleanupFunctions.push(cleanup);
29
+ try {
30
+ cleanupFunctions.push(await plugin.start({ cdp, config, store, logger }));
31
+ }
32
+ catch (error) {
33
+ await runCleanups(cleanupFunctions, logger);
34
+ throw error;
35
+ }
15
36
  }
16
37
  logger.debug(sessionCollectorsActivated(store.activeTelemetry));
17
38
  return cleanupFunctions;
18
39
  }
40
+ /**
41
+ * Run cleanup functions, logging (not throwing) their failures.
42
+ *
43
+ * @param cleanups - Cleanup functions
44
+ * @param logger - Logger
45
+ */
46
+ async function runCleanups(cleanups, logger) {
47
+ for (const cleanup of cleanups) {
48
+ try {
49
+ await cleanup();
50
+ }
51
+ catch (error) {
52
+ logger.debug(`Collector cleanup error: ${getErrorMessage(error)}`);
53
+ }
54
+ }
55
+ }
19
56
  //# sourceMappingURL=collectors.js.map
@@ -1,11 +1,24 @@
1
1
  import type { TelemetryStore } from './TelemetryStore.js';
2
2
  import type { CDPConnection } from '../../connection/cdp.js';
3
+ import { CommandError } from '../../errors/index.js';
3
4
  import type { CommandName, CommandSchemas } from '../../ipc/index.js';
4
5
  import { type SessionEmulation } from '../../runtime/page/emulation.js';
5
- type Handler<K extends CommandName> = (cdp: CDPConnection, params: CommandSchemas[K]['requestSchema']) => Promise<CommandSchemas[K]['responseSchema']>;
6
+ /** A command's handler; `abandoned` aborts when its client disconnects */
7
+ type Handler<K extends CommandName> = (cdp: CDPConnection, params: CommandSchemas[K]['requestSchema'], abandoned?: AbortSignal) => Promise<CommandSchemas[K]['responseSchema']>;
6
8
  export type CommandRegistry = {
7
9
  [K in CommandName]: Handler<K>;
8
10
  };
11
+ /**
12
+ * A `bdg cdp` failure that is the caller's: a method this Chrome doesn't
13
+ * implement (83), wrong parameters (81), or an id of a node, target or frame
14
+ * that does not exist (83). Other failures (internal errors, a detached
15
+ * page) stay software errors.
16
+ *
17
+ * @param method - CDP method
18
+ * @param error - What the call threw
19
+ * @returns The error to report, or undefined to keep the original
20
+ */
21
+ export declare function callerError(method: string, error: unknown): CommandError | undefined;
9
22
  /** The session's page emulation, which `page emulate` reads and changes */
10
23
  export interface EmulationState {
11
24
  get: () => SessionEmulation;