browser-debugger-cli 0.11.0 → 0.13.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 (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -1,4 +1,15 @@
1
1
  import type { Command } from 'commander';
2
+ /**
3
+ * Wait until the stopped session's daemon has exited: it removes the
4
+ * session's files on the way out, and a command run right after (`bdg
5
+ * sessions`, a new start) would otherwise still see the session. The wait is
6
+ * bounded, as after a failed start.
7
+ *
8
+ * @param pid - Daemon PID read before the stop, or null when unknown
9
+ * @param waitMs - Milliseconds to wait at most
10
+ * @returns Warning when the daemon still runs after the wait
11
+ */
12
+ export declare function waitForDaemonExit(pid: number | null, waitMs?: number): Promise<string | undefined>;
2
13
  /**
3
14
  * Register stop command
4
15
  *
@@ -3,13 +3,33 @@ import { jsonOption } from './shared/commonOptions.js';
3
3
  import { stopSession } from '../ipc/client.js';
4
4
  import { IPCErrorCode } from '../ipc/index.js';
5
5
  import { IPCTimeoutError } from '../ipc/transport/index.js';
6
+ import { readLiveDaemonPid } from '../session/cleanup/staleSession.js';
6
7
  import { joinLines } from '../ui/formatting.js';
7
8
  import { chromeClosedMessage, orphanedDaemonsCleanedMessage, warningMessage, } from '../ui/messages/commands.js';
8
- import { sessionStopped, STOP_MESSAGES, stopFailedError } from '../ui/messages/session.js';
9
+ import { daemonStillExitingHint, daemonStillExitingSuggestion, sessionStopped, STOP_MESSAGES, stopFailedError, } from '../ui/messages/session.js';
9
10
  import { noActiveSessionMessage, sessionCommand, startSessionSuggestion, } from '../ui/messages/sessionCommand.js';
11
+ import { waitUntil } from '../utils/async.js';
10
12
  import { getExitCodeForIPCError, isDaemonNotRunningError } from '../utils/errorMapping.js';
11
13
  import { getErrorMessage } from '../utils/errors.js';
12
14
  import { EXIT_CODES } from '../utils/exitCodes.js';
15
+ import { isProcessAlive } from '../utils/process.js';
16
+ /** How long `bdg stop` waits for the session's daemon to exit */
17
+ const DAEMON_EXIT_WAIT_MS = 3000;
18
+ /**
19
+ * Wait until the stopped session's daemon has exited: it removes the
20
+ * session's files on the way out, and a command run right after (`bdg
21
+ * sessions`, a new start) would otherwise still see the session. The wait is
22
+ * bounded, as after a failed start.
23
+ *
24
+ * @param pid - Daemon PID read before the stop, or null when unknown
25
+ * @param waitMs - Milliseconds to wait at most
26
+ * @returns Warning when the daemon still runs after the wait
27
+ */
28
+ export async function waitForDaemonExit(pid, waitMs = DAEMON_EXIT_WAIT_MS) {
29
+ if (pid === null || (await waitUntil(() => !isProcessAlive(pid), waitMs)))
30
+ return undefined;
31
+ return `${daemonStillExitingHint(pid, waitMs)}; ${daemonStillExitingSuggestion()}`;
32
+ }
13
33
  /**
14
34
  * Format stop result for human-readable output.
15
35
  *
@@ -36,8 +56,10 @@ export function registerStopCommand(program) {
36
56
  .action(async (options) => {
37
57
  await runCommand(async () => {
38
58
  try {
59
+ const daemonPid = readLiveDaemonPid();
39
60
  const response = await stopSession();
40
61
  if (response.status === 'ok') {
62
+ const warning = await waitForDaemonExit(daemonPid);
41
63
  return {
42
64
  success: true,
43
65
  data: {
@@ -48,6 +70,7 @@ export function registerStopCommand(program) {
48
70
  },
49
71
  orphanedDaemonsCount: 0,
50
72
  message: response.message ?? STOP_MESSAGES.SUCCESS,
73
+ ...(warning && { warnings: [warning] }),
51
74
  },
52
75
  };
53
76
  }
@@ -1,6 +1,12 @@
1
1
  /**
2
- * Tail command for continuous session monitoring.
2
+ * `bdg tail`: deprecated alias of `bdg peek --follow` (#115). It keeps its
3
+ * options and says to use `peek --follow` instead.
3
4
  */
4
5
  import type { Command } from 'commander';
6
+ /**
7
+ * Register the deprecated `tail` command.
8
+ *
9
+ * @param program - Root command
10
+ */
5
11
  export declare function registerTailCommand(program: Command): void;
6
12
  //# sourceMappingURL=tail.d.ts.map
@@ -1,43 +1,19 @@
1
1
  /**
2
- * Tail command for continuous session monitoring.
2
+ * `bdg tail`: deprecated alias of `bdg peek --follow` (#115). It keeps its
3
+ * options and says to use `peek --follow` instead.
4
+ */
5
+ import { followPreview } from './peek.js';
6
+ import { jsonOption } from './shared/commonOptions.js';
7
+ import { tailDeprecatedNotice } from '../ui/messages/preview.js';
8
+ /**
9
+ * Register the deprecated `tail` command.
10
+ *
11
+ * @param program - Root command
3
12
  */
4
- import { jsonOption, showBothSectionsWhenBothRequested } from './shared/commonOptions.js';
5
- import { noteFollowConnected } from './shared/daemonErrorHandler.js';
6
- import { fetchPreviewOutput } from './shared/dataFetcher.js';
7
- import { followFetchFailure, setupFollowMode, } from './shared/followMode.js';
8
- import { handleValidationError } from './shared/handleValidationError.js';
9
- import { MAX_LAST_ITEMS, positiveIntRule } from './shared/validation.js';
10
- import { formatPreview } from '../ui/formatters/preview.js';
11
- import { followingPreviewMessage, stoppedFollowingPreviewMessage } from '../ui/messages/preview.js';
12
- function parseOptions(options) {
13
- const lastRule = positiveIntRule({
14
- name: '--last',
15
- min: 1,
16
- max: MAX_LAST_ITEMS,
17
- default: 10,
18
- allowZeroForAll: true,
19
- });
20
- const intervalRule = positiveIntRule({ name: '--interval', min: 100, max: 60000, default: 1000 });
21
- return {
22
- lastN: lastRule.validate(options.last),
23
- interval: intervalRule.validate(options.interval),
24
- };
25
- }
26
- function createPreviewOptions(options, lastN) {
27
- return {
28
- json: options.json,
29
- network: options.network,
30
- console: options.console,
31
- last: lastN,
32
- verbose: options.verbose,
33
- follow: true,
34
- viewedAt: new Date(),
35
- };
36
- }
37
13
  export function registerTailCommand(program) {
38
14
  program
39
15
  .command('tail')
40
- .description('Continuously monitor session data (like tail -f)')
16
+ .description('Deprecated: use "bdg peek --follow" (same options)')
41
17
  .addOption(jsonOption())
42
18
  .option('-v, --verbose', 'Use verbose output with full URLs and formatting', false)
43
19
  .option('-n, --network', 'Show only network requests', false)
@@ -45,33 +21,8 @@ export function registerTailCommand(program) {
45
21
  .option('--last <count>', 'Show last N items (network requests + console messages), 0 for all', '10')
46
22
  .option('--interval <ms>', 'Update interval in milliseconds', '1000')
47
23
  .action(async (options) => {
48
- showBothSectionsWhenBothRequested(options);
49
- let lastN;
50
- let interval;
51
- try {
52
- const parsed = parseOptions(options);
53
- lastN = parsed.lastN;
54
- interval = parsed.interval;
55
- }
56
- catch (error) {
57
- handleValidationError(error, options.json ?? false);
58
- }
59
- const showPreview = async () => {
60
- const result = await fetchPreviewOutput({ lastN });
61
- if (!result.success) {
62
- return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
63
- }
64
- noteFollowConnected();
65
- if (!options.json)
66
- console.clear();
67
- console.log(formatPreview(result.data, createPreviewOptions(options, lastN)));
68
- return undefined;
69
- };
70
- await setupFollowMode(showPreview, {
71
- startMessage: followingPreviewMessage,
72
- stopMessage: stoppedFollowingPreviewMessage,
73
- intervalMs: interval,
74
- });
24
+ console.error(tailDeprecatedNotice());
25
+ await followPreview({ ...options, follow: true });
75
26
  });
76
27
  }
77
28
  //# sourceMappingURL=tail.js.map
package/dist/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { registerCdpCommand } from './commands/cdp.js';
2
2
  import { registerCleanupCommand } from './commands/cleanup.js';
3
3
  import { registerConsoleCommand } from './commands/console.js';
4
+ import { registerCssCommands } from './commands/css.js';
4
5
  import { registerDetailsCommand } from './commands/details.js';
5
6
  import { registerFormInteractionCommands } from './commands/dom/formInteraction.js';
6
7
  import { registerDomCommands } from './commands/dom/index.js';
@@ -39,6 +40,7 @@ export const commandRegistry = [
39
40
  registerDomCommands,
40
41
  registerFormInteractionCommands,
41
42
  registerPageCommands,
43
+ registerCssCommands,
42
44
  addCommandGroup('CDP Commands:'),
43
45
  registerCdpCommand,
44
46
  addCommandGroup('Network Commands:'),
@@ -181,6 +181,13 @@ export declare class CDPConnection implements CDPEventSource {
181
181
  * @returns Calculated delay in milliseconds (base delay * 2^attempt, capped at maxDelay)
182
182
  */
183
183
  private calculateBackoffDelay;
184
+ /**
185
+ * Fail every command still waiting for an answer, e.g. after the page's
186
+ * renderer crashed: commands it was running are never answered.
187
+ *
188
+ * @param error - Error the waiting commands fail with
189
+ */
190
+ rejectPending(error: Error): void;
184
191
  /**
185
192
  * Clear all pending command promises with the given error.
186
193
  *
@@ -390,6 +390,15 @@ export class CDPConnection {
390
390
  calculateBackoffDelay(attempt, maxDelay) {
391
391
  return Math.min(this.config.baseRetryDelay * Math.pow(2, attempt), maxDelay);
392
392
  }
393
+ /**
394
+ * Fail every command still waiting for an answer, e.g. after the page's
395
+ * renderer crashed: commands it was running are never answered.
396
+ *
397
+ * @param error - Error the waiting commands fail with
398
+ */
399
+ rejectPending(error) {
400
+ this.clearPendingMessages(error);
401
+ }
393
402
  /**
394
403
  * Clear all pending command promises with the given error.
395
404
  *
@@ -3,6 +3,7 @@ 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, DEFAULT_CHROME_HANDLE_SIGINT, } from '../constants.js';
6
+ import { makeDirectory } from '../utils/directories.js';
6
7
  import { getErrorMessage } from '../utils/errors.js';
7
8
  import { filterDefined } from '../utils/objects.js';
8
9
  import { isProcessAlive } from '../utils/process.js';
@@ -62,7 +63,7 @@ export async function launchChrome(options = {}) {
62
63
  const userDataDir = options.userDataDir ?? getPersistentUserDataDir(options.baseDir ?? options.sessionDir);
63
64
  if (!fs.existsSync(userDataDir)) {
64
65
  try {
65
- fs.mkdirSync(userDataDir, { recursive: true });
66
+ makeDirectory(userDataDir);
66
67
  }
67
68
  catch (error) {
68
69
  throw new ChromeLaunchError(`Failed to create user data directory`, {
@@ -160,7 +161,7 @@ function getPersistentUserDataDir(baseDir) {
160
161
  const userDataDir = path.join(dir, CHROME_PROFILE_DIR);
161
162
  if (!fs.existsSync(userDataDir)) {
162
163
  try {
163
- fs.mkdirSync(userDataDir, { recursive: true });
164
+ makeDirectory(userDataDir);
164
165
  }
165
166
  catch (error) {
166
167
  throw new ChromeLaunchError(`Failed to create user data directory`, {
@@ -241,8 +241,13 @@ export class SessionController {
241
241
  duration: data.duration,
242
242
  target: data.target,
243
243
  data: { network: data.network, console: data.console },
244
- totals: { network: data.totalNetwork, console: data.totalConsole },
244
+ totals: {
245
+ network: data.totalNetwork,
246
+ console: data.totalConsole,
247
+ ...(data.droppedConsole && { consoleDropped: data.droppedConsole }),
248
+ },
245
249
  currentNavigationId: data.currentNavigationId,
250
+ ...(data.pageCrashedAt !== undefined && { pageCrashedAt: data.pageCrashedAt }),
246
251
  partial: true,
247
252
  },
248
253
  },
@@ -24,8 +24,9 @@ export declare function launchDaemon(): Promise<SpawnedDaemon | undefined>;
24
24
  * Check that the session directory can hold the daemon's files before
25
25
  * spawning it (otherwise the daemon dies and only its log says why).
26
26
  *
27
- * @throws SessionDirError (103) for a file, (81) for a too-long path like a
28
- * named session's, (82) when not writable
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
29
30
  */
30
31
  export declare function assertUsableSessionDir(): void;
31
32
  //# sourceMappingURL=launcher.d.ts.map
@@ -11,13 +11,16 @@ import fs from 'fs';
11
11
  import { join } from 'path';
12
12
  import { DaemonStartupError, SessionDirError } from './errors.js';
13
13
  import { sessionDirIsFileError, sessionDirNotWritableError, socketPathTooLongError, } from '../errors/messages.js';
14
- import { isDaemonAlive } from '../session/daemonSocket.js';
14
+ import { killSessionChromes, readLiveDaemonPid } from '../session/cleanup/staleSession.js';
15
+ import { isDaemonAlive, isDaemonSocketGone } from '../session/daemonSocket.js';
15
16
  import { MAX_DAEMON_SOCKET_PATH_BYTES, ensureSessionDir, getSessionDir, getSessionFilePath, } from '../session/paths.js';
16
17
  import { createLogger } from '../ui/logging/index.js';
17
18
  import { delay } from '../utils/async.js';
19
+ import { directoryProblem } from '../utils/directories.js';
18
20
  import { getErrorMessage } from '../utils/errors.js';
19
21
  import { EXIT_CODES } from '../utils/exitCodes.js';
20
22
  import { DAEMON_SCRIPT_PATH } from '../utils/packageRoot.js';
23
+ import { isProcessAlive } from '../utils/process.js';
21
24
  const log = createLogger('launcher');
22
25
  const DAEMON_READY_TIMEOUT_MS = 5000;
23
26
  const DAEMON_READY_POLL_MS = 20;
@@ -37,6 +40,7 @@ export async function launchDaemon() {
37
40
  throw new DaemonStartupError(`Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`, 'DAEMON_SCRIPT_NOT_FOUND');
38
41
  }
39
42
  assertUsableSessionDir();
43
+ await stopUnreachableDaemon();
40
44
  const logPath = join(getSessionDir(), 'daemon.log');
41
45
  rotateLog(logPath);
42
46
  const logFd = fs.openSync(logPath, 'a');
@@ -54,12 +58,48 @@ export async function launchDaemon() {
54
58
  await waitForDaemonReady(() => exited);
55
59
  return { pid: daemon.pid, hasExited: () => exited };
56
60
  }
61
+ /** How long a daemon that lost its socket gets to end its session before it is killed */
62
+ const UNREACHABLE_DAEMON_EXIT_MS = 8000;
63
+ /**
64
+ * Stop a daemon of this session directory that still runs but can no longer
65
+ * be reached (its socket was removed, or refuses connections; one that is
66
+ * only slow to answer is left alone), before a new one starts: two daemons
67
+ * would launch two Chromes and remove each other's files. It is asked to end
68
+ * its session (closing its Chrome), and killed with what it launched if it
69
+ * does not exit in time.
70
+ */
71
+ async function stopUnreachableDaemon() {
72
+ const pid = readLiveDaemonPid();
73
+ if (pid === null || !(await isDaemonSocketGone()))
74
+ return;
75
+ log.info(`Stopping daemon ${pid}, which can no longer be reached (its socket is gone)`);
76
+ try {
77
+ process.kill(pid, 'SIGTERM');
78
+ }
79
+ catch (error) {
80
+ log.debug(`Daemon ${pid} not signalled: ${getErrorMessage(error)}`);
81
+ return;
82
+ }
83
+ const deadline = Date.now() + UNREACHABLE_DAEMON_EXIT_MS;
84
+ while (isProcessAlive(pid) && Date.now() < deadline)
85
+ await delay(100);
86
+ if (!isProcessAlive(pid))
87
+ return;
88
+ try {
89
+ process.kill(pid, 'SIGKILL');
90
+ }
91
+ catch (error) {
92
+ log.debug(`Daemon ${pid} not killed: ${getErrorMessage(error)}`);
93
+ }
94
+ killSessionChromes();
95
+ }
57
96
  /**
58
97
  * Check that the session directory can hold the daemon's files before
59
98
  * spawning it (otherwise the daemon dies and only its log says why).
60
99
  *
61
- * @throws SessionDirError (103) for a file, (81) for a too-long path like a
62
- * named session's, (82) when not writable
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
63
103
  */
64
104
  export function assertUsableSessionDir() {
65
105
  const dir = getSessionDir();
@@ -72,6 +112,10 @@ export function assertUsableSessionDir() {
72
112
  if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
73
113
  fail(socketPathTooLongError(socketPath, MAX_DAEMON_SOCKET_PATH_BYTES), EXIT_CODES.INVALID_ARGUMENTS);
74
114
  }
115
+ const problem = directoryProblem(dir);
116
+ if (problem) {
117
+ fail(sessionDirNotWritableError(dir, problem.reason), problem.denied ? EXIT_CODES.PERMISSION_DENIED : EXIT_CODES.SESSION_FILE_ERROR);
118
+ }
75
119
  try {
76
120
  ensureSessionDir();
77
121
  fs.accessSync(dir, fs.constants.W_OK);
@@ -80,7 +80,10 @@ export declare class Session {
80
80
  */
81
81
  launch(): Promise<void>;
82
82
  /**
83
- * Execute a registered command against this session.
83
+ * Execute a registered command against this session. After a renderer
84
+ * crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
85
+ * {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
86
+ * waiting for a page that cannot answer.
84
87
  *
85
88
  * @param name - Command name
86
89
  * @param params - Command parameters
@@ -18,6 +18,7 @@ import { readPageLoadingState } from '../../runtime/page/loadingState.js';
18
18
  import { reapOrphanedChrome, removeSessionFiles } from '../../session/cleanup/staleSession.js';
19
19
  import { writeSessionMetadata } from '../../session/metadata.js';
20
20
  import { getSessionPort } from '../../session/port.js';
21
+ import { pageCrashedCommandError } from '../../telemetry/pageCrash.js';
21
22
  import { createLogger } from '../../ui/logging/index.js';
22
23
  import { formatChromeNotice } from '../../ui/messages/chrome.js';
23
24
  import { delay } from '../../utils/async.js';
@@ -31,6 +32,26 @@ const log = createLogger('session');
31
32
  const CRASH_SETTLE_MS = 500;
32
33
  /** Ports tried when automatically chosen ports turn out to be taken */
33
34
  const PORT_ATTEMPTS = 3;
35
+ /**
36
+ * Commands that still work after the page's renderer crashed: they read what
37
+ * the session collected, load the page again (`page reload`/`navigate`) or
38
+ * send raw CDP. Every other command needs the page and fails at once.
39
+ */
40
+ const RUN_ON_CRASHED_PAGE = new Set([
41
+ 'session_peek',
42
+ 'session_details',
43
+ 'session_status',
44
+ 'session_har_data',
45
+ 'session_network_headers',
46
+ 'page_navigate',
47
+ 'cdp_call',
48
+ ]);
49
+ /**
50
+ * CDP methods `bdg cdp` may still send after a renderer crash: loading the
51
+ * page again, and domains the browser process answers. Others wait for the
52
+ * renderer, which never answers.
53
+ */
54
+ const CRASH_SAFE_CDP = /^(Page\.(navigate|reload|getNavigationHistory|navigateToHistoryEntry)|Target\.|Browser\.|Inspector\.|Network\.(?!getResponseBody|getRequestPostData)|Storage\.|SystemInfo\.)/;
34
55
  /**
35
56
  * Whether a launch failed because another process holds the port.
36
57
  *
@@ -127,7 +148,10 @@ export class Session {
127
148
  }
128
149
  }
129
150
  /**
130
- * Execute a registered command against this session.
151
+ * Execute a registered command against this session. After a renderer
152
+ * crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
153
+ * {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
154
+ * waiting for a page that cannot answer.
131
155
  *
132
156
  * @param name - Command name
133
157
  * @param params - Command parameters
@@ -137,6 +161,13 @@ export class Session {
137
161
  if (!this.cdp || !this.started || this.stopping) {
138
162
  return Promise.reject(new Error('No active session'));
139
163
  }
164
+ const crashedAt = this.store.pageCrashedAt;
165
+ const cdpMethod = name === 'cdp_call' ? params.method : undefined;
166
+ const needsPage = !RUN_ON_CRASHED_PAGE.has(name) ||
167
+ (cdpMethod !== undefined && !CRASH_SAFE_CDP.test(cdpMethod));
168
+ if (crashedAt !== undefined && needsPage) {
169
+ return Promise.reject(pageCrashedCommandError(crashedAt));
170
+ }
140
171
  return this.registry[name](this.cdp, params);
141
172
  }
142
173
  /**
@@ -223,7 +254,7 @@ export class Session {
223
254
  await this.releaseResources();
224
255
  if (!this.started)
225
256
  return;
226
- removeSessionFiles();
257
+ removeSessionFiles(process.pid);
227
258
  this.onEnded(reason);
228
259
  })();
229
260
  return this.stopping;
@@ -7,11 +7,18 @@ export declare class TelemetryStore {
7
7
  /** Requests still in flight, keyed by CDP requestId */
8
8
  readonly pendingNetworkRequests: Map<string, PendingRequest>;
9
9
  readonly consoleMessages: ConsoleMessage[];
10
+ /**
11
+ * Console messages dropped at the limit, oldest first: the index of
12
+ * `consoleMessages[i]` in the session is `consoleDropped + i`
13
+ */
14
+ consoleDropped: number;
10
15
  readonly navigationEvents: NavigationEvent[];
11
16
  readonly websocketConnections: WebSocketConnection[];
12
17
  /** JavaScript dialogs accepted during the session */
13
18
  readonly dialogs: DialogInfo[];
14
19
  activeTelemetry: TelemetryType[];
20
+ /** When the page's renderer crashed (epoch ms); undefined while the page is alive */
21
+ pageCrashedAt: number | undefined;
15
22
  getCurrentNavigationId: (() => number) | null;
16
23
  sessionStartTime: number;
17
24
  targetInfo: CDPTarget | null;
@@ -21,7 +28,7 @@ export declare class TelemetryStore {
21
28
  /**
22
29
  * Record an accepted dialog, and show it among the console messages when
23
30
  * console telemetry is collected (it is otherwise invisible: bdg accepts it
24
- * before anyone could see it).
31
+ * before anyone could see it), dropping the oldest message at the limit.
25
32
  *
26
33
  * @param dialog - Dialog type and text
27
34
  */
@@ -1,14 +1,22 @@
1
+ import { MAX_CONSOLE_MESSAGES } from '../../constants.js';
1
2
  import { dialogConsoleText } from '../../ui/messages/commands.js';
2
3
  export class TelemetryStore {
3
4
  networkRequests = [];
4
5
  /** Requests still in flight, keyed by CDP requestId */
5
6
  pendingNetworkRequests = new Map();
6
7
  consoleMessages = [];
8
+ /**
9
+ * Console messages dropped at the limit, oldest first: the index of
10
+ * `consoleMessages[i]` in the session is `consoleDropped + i`
11
+ */
12
+ consoleDropped = 0;
7
13
  navigationEvents = [];
8
14
  websocketConnections = [];
9
15
  /** JavaScript dialogs accepted during the session */
10
16
  dialogs = [];
11
17
  activeTelemetry = [];
18
+ /** When the page's renderer crashed (epoch ms); undefined while the page is alive */
19
+ pageCrashedAt;
12
20
  getCurrentNavigationId = null;
13
21
  sessionStartTime = Date.now();
14
22
  targetInfo = null;
@@ -24,7 +32,7 @@ export class TelemetryStore {
24
32
  /**
25
33
  * Record an accepted dialog, and show it among the console messages when
26
34
  * console telemetry is collected (it is otherwise invisible: bdg accepts it
27
- * before anyone could see it).
35
+ * before anyone could see it), dropping the oldest message at the limit.
28
36
  *
29
37
  * @param dialog - Dialog type and text
30
38
  */
@@ -32,6 +40,10 @@ export class TelemetryStore {
32
40
  this.dialogs.push(dialog);
33
41
  if (!this.activeTelemetry.includes('console'))
34
42
  return;
43
+ if (this.consoleMessages.length >= MAX_CONSOLE_MESSAGES) {
44
+ this.consoleMessages.shift();
45
+ this.consoleDropped++;
46
+ }
35
47
  this.consoleMessages.push({
36
48
  type: 'info',
37
49
  text: dialogConsoleText(dialog),