browser-debugger-cli 0.12.0 → 0.14.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  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 +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Which browser built-ins a page replaced (polyfills, old frameworks,
3
+ * anti-bot scripts). Scripts that must run in the page's own world (actions,
4
+ * `dom eval` results) depend on them.
5
+ *
6
+ * The check does not trust the page: a small page script only collects the
7
+ * functions (plain property reads and the page's
8
+ * `Object.getOwnPropertyDescriptor`, itself checked), and whether each is
9
+ * native comes from the description CDP gives a function, which V8 builds
10
+ * itself, so a replaced `Function.prototype.toString` or `.call` changes
11
+ * nothing. A getter or setter counts as the property (a page-defined getter
12
+ * on `window.getComputedStyle`, a replaced `value` setter); a property that
13
+ * throws or is gone counts as replaced. An iterator is not checked when the
14
+ * page replaced `Symbol` (iteration does not use it).
15
+ */
16
+ import type { CDPSender } from '../../telemetry/objectExpander.js';
17
+ /**
18
+ * The built-ins of a list that the page replaced.
19
+ *
20
+ * @param cdp - CDP connection (or session) of the page
21
+ * @param names - Dotted paths, e.g. `Element.prototype.matches`,
22
+ * `window.getComputedStyle`, `NodeList.prototype[Symbol.iterator]`
23
+ * @param objectId - An object of the realm to check (e.g. a frame's eval
24
+ * result); default: the page's main world
25
+ * @returns The replaced ones, in list order (empty when the check failed)
26
+ */
27
+ export declare function findReplacedBuiltins(cdp: CDPSender, names: readonly string[], objectId?: string): Promise<string[]>;
28
+ //# sourceMappingURL=replacedBuiltins.d.ts.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Which browser built-ins a page replaced (polyfills, old frameworks,
3
+ * anti-bot scripts). Scripts that must run in the page's own world (actions,
4
+ * `dom eval` results) depend on them.
5
+ *
6
+ * The check does not trust the page: a small page script only collects the
7
+ * functions (plain property reads and the page's
8
+ * `Object.getOwnPropertyDescriptor`, itself checked), and whether each is
9
+ * native comes from the description CDP gives a function, which V8 builds
10
+ * itself, so a replaced `Function.prototype.toString` or `.call` changes
11
+ * nothing. A getter or setter counts as the property (a page-defined getter
12
+ * on `window.getComputedStyle`, a replaced `value` setter); a property that
13
+ * throws or is gone counts as replaced. An iterator is not checked when the
14
+ * page replaced `Symbol` (iteration does not use it).
15
+ */
16
+ import { createLogger } from '../../ui/logging/index.js';
17
+ import { getErrorMessage } from '../../utils/errors.js';
18
+ const log = createLogger('dom');
19
+ /** Description V8 gives a native function, e.g. `function querySelector() { [native code] }` */
20
+ const NATIVE_FUNCTION = /\{\s*\[native code\]\s*\}$/;
21
+ /** Suffix naming a prototype's iterator, e.g. `NodeList.prototype[Symbol.iterator]` */
22
+ const ITERATOR_SUFFIX = '[Symbol.iterator]';
23
+ /** Path segment the collector reads as `Symbol.iterator` */
24
+ const ITERATOR_KEY = '@@iterator';
25
+ /** Object group of the collected functions (released after each check) */
26
+ const OBJECT_GROUP = 'bdg-builtins';
27
+ /**
28
+ * Page-side collector: for each name, the function(s) behind it, as a flat
29
+ * list of `index, function` pairs (`undefined` for one that throws or is
30
+ * gone). Uses no array or object helpers of the page.
31
+ *
32
+ * @param names - Dotted paths from the global object (a leading `window.` is
33
+ * skipped; a trailing `[Symbol.iterator]` names the iterator)
34
+ * @returns Function declaration
35
+ */
36
+ function collectorFunction(names) {
37
+ const paths = names.map((name) => name
38
+ .replace(/^window\./, '')
39
+ .replace(ITERATOR_SUFFIX, `.${ITERATOR_KEY}`)
40
+ .split('.'));
41
+ return `function () {
42
+ const paths = ${JSON.stringify(paths)};
43
+ const describe = Object.getOwnPropertyDescriptor;
44
+ const found = [];
45
+ const add = (i, value) => { found[found.length] = i; found[found.length] = value; };
46
+ for (let i = 0; i < paths.length; i++) {
47
+ const path = paths[i];
48
+ let key = path[path.length - 1];
49
+ let owner = window;
50
+ try {
51
+ for (let j = 0; j < path.length - 1; j++) owner = owner[path[j]];
52
+ } catch (e) { add(i, undefined); continue; }
53
+ if (key === '${ITERATOR_KEY}') {
54
+ try { key = Symbol.iterator; } catch (e) { continue; }
55
+ if (typeof key !== 'symbol') continue;
56
+ }
57
+ let own;
58
+ try { own = describe(owner, key); } catch (e) { own = undefined; }
59
+ if (own && (own.get || own.set)) {
60
+ if (own.get) add(i, own.get);
61
+ if (own.set) add(i, own.set);
62
+ } else if (own) {
63
+ add(i, own.value);
64
+ } else {
65
+ try { add(i, owner[key]); } catch (e) { continue; }
66
+ }
67
+ }
68
+ return found;
69
+ }`;
70
+ }
71
+ /**
72
+ * The built-ins of a list that the page replaced.
73
+ *
74
+ * @param cdp - CDP connection (or session) of the page
75
+ * @param names - Dotted paths, e.g. `Element.prototype.matches`,
76
+ * `window.getComputedStyle`, `NodeList.prototype[Symbol.iterator]`
77
+ * @param objectId - An object of the realm to check (e.g. a frame's eval
78
+ * result); default: the page's main world
79
+ * @returns The replaced ones, in list order (empty when the check failed)
80
+ */
81
+ export async function findReplacedBuiltins(cdp, names, objectId) {
82
+ const functionDeclaration = collectorFunction(names);
83
+ try {
84
+ const collected = (await (objectId
85
+ ? cdp.send('Runtime.callFunctionOn', {
86
+ objectId,
87
+ functionDeclaration,
88
+ objectGroup: OBJECT_GROUP,
89
+ })
90
+ : cdp.send('Runtime.evaluate', {
91
+ expression: `(${functionDeclaration})()`,
92
+ objectGroup: OBJECT_GROUP,
93
+ })));
94
+ const listId = collected.result.objectId;
95
+ if (collected.exceptionDetails || !listId)
96
+ return [];
97
+ const { result } = (await cdp.send('Runtime.getProperties', {
98
+ objectId: listId,
99
+ ownProperties: true,
100
+ }));
101
+ return replacedNames(names, result);
102
+ }
103
+ catch (error) {
104
+ log.debug(`Built-ins not checked: ${getErrorMessage(error)}`);
105
+ return [];
106
+ }
107
+ finally {
108
+ await cdp
109
+ .send('Runtime.releaseObjectGroup', { objectGroup: OBJECT_GROUP })
110
+ .catch((error) => log.debug(`Built-ins not released: ${getErrorMessage(error)}`));
111
+ }
112
+ }
113
+ /**
114
+ * Names whose collected function is not native.
115
+ *
116
+ * @param names - The names checked
117
+ * @param properties - Properties of the collected `index, function` list
118
+ * @returns Replaced names, in list order
119
+ */
120
+ function replacedNames(names, properties) {
121
+ const entries = new Map();
122
+ for (const property of properties) {
123
+ if (/^\d+$/.test(property.name))
124
+ entries.set(Number(property.name), property.value);
125
+ }
126
+ const replaced = new Set();
127
+ for (let i = 0; entries.has(i); i += 2) {
128
+ const index = entries.get(i)?.value;
129
+ const value = entries.get(i + 1);
130
+ const native = value?.type === 'function' && NATIVE_FUNCTION.test(value.description ?? '');
131
+ if (typeof index === 'number' && !native)
132
+ replaced.add(index);
133
+ }
134
+ return names.filter((_name, i) => replaced.has(i));
135
+ }
136
+ //# sourceMappingURL=replacedBuiltins.js.map
@@ -71,8 +71,11 @@ export declare class QueryCacheManager {
71
71
  * Store a query result.
72
72
  *
73
73
  * @param result - Query result whose nodes carry backend node ids
74
+ * @param document - Identity of the page document queried
75
+ * ({@link pageDocumentId}): backend node ids are only meaningful in it, and
76
+ * a page in a new renderer process reuses them for other elements
74
77
  */
75
- set(result: DomQueryResult): Promise<void>;
78
+ set(result: DomQueryResult, document?: string): Promise<void>;
76
79
  /**
77
80
  * Read the cache and check that it can be used.
78
81
  *
@@ -88,11 +88,14 @@ export class QueryCacheManager {
88
88
  * Store a query result.
89
89
  *
90
90
  * @param result - Query result whose nodes carry backend node ids
91
+ * @param document - Identity of the page document queried
92
+ * ({@link pageDocumentId}): backend node ids are only meaningful in it, and
93
+ * a page in a new renderer process reuses them for other elements
91
94
  */
92
- async set(result) {
95
+ async set(result, document) {
93
96
  try {
94
97
  const cachePath = this.getCachePath();
95
- await writeFile(cachePath, JSON.stringify({ version: CACHE_VERSION, ...result }), 'utf-8');
98
+ await writeFile(cachePath, JSON.stringify({ version: CACHE_VERSION, ...result, ...(document && { document }) }), 'utf-8');
96
99
  log.debug(`Cached ${result.nodes.length} query results to ${cachePath}`);
97
100
  }
98
101
  catch (error) {
@@ -41,8 +41,11 @@ export declare function readChromePid(): number | null;
41
41
  * Remove Chrome PID from persistent cache.
42
42
  *
43
43
  * Safe to call multiple times (idempotent).
44
+ *
45
+ * @param expectedPid - Remove it only while it still names this Chrome (a
46
+ * daemon clearing its own, which another daemon may have replaced)
44
47
  */
45
- export declare function clearChromePid(): void;
48
+ export declare function clearChromePid(expectedPid?: number): void;
46
49
  /** What `status --verbose` shows about the session's Chrome */
47
50
  export interface RunningChromeInfo {
48
51
  /** Executable the process was started from */
@@ -10,6 +10,7 @@ import { AtomicFileWriter } from '../utils/atomicFile.js';
10
10
  import { getErrorMessage } from '../utils/errors.js';
11
11
  import { getProcessCommand, isProcessAlive } from '../utils/process.js';
12
12
  import { getSessionFilePath, ensureSessionDir } from './paths.js';
13
+ import { readPidFromFile } from './pid.js';
13
14
  const log = createLogger('chrome');
14
15
  /**
15
16
  * Parse Chrome PID from cache file content.
@@ -100,9 +101,14 @@ export function readChromePid() {
100
101
  * Remove Chrome PID from persistent cache.
101
102
  *
102
103
  * Safe to call multiple times (idempotent).
104
+ *
105
+ * @param expectedPid - Remove it only while it still names this Chrome (a
106
+ * daemon clearing its own, which another daemon may have replaced)
103
107
  */
104
- export function clearChromePid() {
108
+ export function clearChromePid(expectedPid) {
105
109
  const cachePath = getSessionFilePath('CHROME_PID');
110
+ if (expectedPid !== undefined && readPidFromFile(cachePath) !== expectedPid)
111
+ return;
106
112
  try {
107
113
  fs.rmSync(cachePath, { force: true });
108
114
  }
@@ -9,9 +9,13 @@
9
9
  /**
10
10
  * Remove per-session files (metadata, query cache).
11
11
  *
12
- * Called by the daemon when its session ends, and by `bdg cleanup`.
12
+ * Called by the daemon when its session ends, and by `bdg cleanup`. A daemon
13
+ * passes its PID, so it never removes the files of another daemon that
14
+ * started in the same directory meanwhile.
15
+ *
16
+ * @param ownerPid - The daemon whose files these must be; omitted by cleanup
13
17
  */
14
- export declare function removeSessionFiles(): void;
18
+ export declare function removeSessionFiles(ownerPid?: number): void;
15
19
  /**
16
20
  * Remove daemon and session files if no daemon is listening.
17
21
  *
@@ -43,8 +47,21 @@ export declare function isSessionChrome(pid: number, sessionDir: string): boolea
43
47
  */
44
48
  export declare function killOrphanedChrome(): boolean;
45
49
  /**
46
- * Kill an orphaned Chrome (see {@link killOrphanedChrome}) and wait until it
47
- * has exited, so its debugging port is free for the next launch.
50
+ * Kill every Chrome launched for a session directory (its marker flag on the
51
+ * command line), whatever chrome.pid says: a Chrome whose PID file another
52
+ * daemon removed, or that a second start left behind, is found too. Only
53
+ * browser processes are signalled; their helper processes exit with them.
54
+ *
55
+ * @param sessionDir - Session directory
56
+ * @returns PIDs of the processes killed
57
+ */
58
+ export declare function killSessionChromes(sessionDir?: string): number[];
59
+ /**
60
+ * Kill the Chromes this session left behind and wait until they have exited,
61
+ * so their profile and debugging port are free for the next launch. The
62
+ * Chrome recorded in chrome.pid is killed (see {@link killOrphanedChrome});
63
+ * without a record (chrome.pid lost with its daemon), every Chrome carrying
64
+ * the session's marker is, unless another daemon of the session still runs.
48
65
  *
49
66
  * @returns True if a Chrome process was killed
50
67
  */
@@ -10,13 +10,14 @@ import { chromeSessionMarkerFlag } from '../../connection/launcher/flagsBuilder.
10
10
  import { QueryCacheManager } from '../QueryCacheManager.js';
11
11
  import { clearChromePid, readChromePid } from '../chrome.js';
12
12
  import { probeDaemonSocket } from '../daemonSocket.js';
13
+ import { readSessionMetadata } from '../metadata.js';
13
14
  import { getSessionDir, getSessionFilePath, sessionFilePathIn } from '../paths.js';
14
15
  import { readPidFromFile } from '../pid.js';
15
16
  import { createLogger, logDebugError } from '../../ui/logging/index.js';
16
17
  import { delay } from '../../utils/async.js';
17
18
  import { safeRemoveFile } from '../../utils/file.js';
18
19
  import { DAEMON_SCRIPT_PATH } from '../../utils/packageRoot.js';
19
- import { getProcessCommand, isProcessAlive, killChromeProcess } from '../../utils/process.js';
20
+ import { getProcessCommand, isProcessAlive, killChromeProcess, listProcesses, } from '../../utils/process.js';
20
21
  const log = createLogger('cleanup');
21
22
  /**
22
23
  * Check whether a process command line contains an exact argument.
@@ -30,12 +31,39 @@ function hasArgument(command, arg) {
30
31
  return false;
31
32
  return command.includes(`${arg} `) || command.endsWith(arg);
32
33
  }
34
+ /**
35
+ * Check whether a command line contains an exact argument, followed by the
36
+ * end or by another option: a session directory with a space in it
37
+ * (`/tmp/a b`) does not match the marker of `/tmp/a`.
38
+ *
39
+ * @param command - Full command line
40
+ * @param arg - Argument to look for
41
+ * @returns True if `arg` appears as a whole argument
42
+ */
43
+ function hasWholeArgument(command, arg) {
44
+ for (let at = command.indexOf(arg); at !== -1; at = command.indexOf(arg, at + 1)) {
45
+ const before = at === 0 || command[at - 1] === ' ';
46
+ const rest = command.slice(at + arg.length);
47
+ if (before && (rest === '' || rest.startsWith(' -')))
48
+ return true;
49
+ }
50
+ return false;
51
+ }
33
52
  /**
34
53
  * Remove per-session files (metadata, query cache).
35
54
  *
36
- * Called by the daemon when its session ends, and by `bdg cleanup`.
55
+ * Called by the daemon when its session ends, and by `bdg cleanup`. A daemon
56
+ * passes its PID, so it never removes the files of another daemon that
57
+ * started in the same directory meanwhile.
58
+ *
59
+ * @param ownerPid - The daemon whose files these must be; omitted by cleanup
37
60
  */
38
- export function removeSessionFiles() {
61
+ export function removeSessionFiles(ownerPid) {
62
+ if (ownerPid !== undefined) {
63
+ const owner = readSessionMetadata()?.bdgPid;
64
+ if (owner !== undefined && owner !== ownerPid)
65
+ return;
66
+ }
39
67
  safeRemoveFile(getSessionFilePath('METADATA'), 'metadata file', log);
40
68
  void QueryCacheManager.getInstance()
41
69
  .clear()
@@ -103,22 +131,64 @@ export function killOrphanedChrome() {
103
131
  clearChromePid();
104
132
  return true;
105
133
  }
134
+ /**
135
+ * Kill every Chrome launched for a session directory (its marker flag on the
136
+ * command line), whatever chrome.pid says: a Chrome whose PID file another
137
+ * daemon removed, or that a second start left behind, is found too. Only
138
+ * browser processes are signalled; their helper processes exit with them.
139
+ *
140
+ * @param sessionDir - Session directory
141
+ * @returns PIDs of the processes killed
142
+ */
143
+ export function killSessionChromes(sessionDir = getSessionDir()) {
144
+ const marker = chromeSessionMarkerFlag(sessionDir);
145
+ const killed = [];
146
+ for (const { pid, command } of listProcesses()) {
147
+ if (pid === process.pid || !hasWholeArgument(command, marker) || / --type=/.test(command)) {
148
+ continue;
149
+ }
150
+ try {
151
+ killChromeProcess(pid, 'SIGKILL');
152
+ killed.push(pid);
153
+ log.info(`Killed Chrome of this session (PID ${pid})`);
154
+ }
155
+ catch (error) {
156
+ logDebugError(log, `kill Chrome ${pid}`, error);
157
+ }
158
+ }
159
+ if (killed.length > 0)
160
+ clearChromePid();
161
+ return killed;
162
+ }
106
163
  /** How long to wait for a killed orphaned Chrome to exit (and free its port) */
107
164
  const ORPHAN_EXIT_WAIT_MS = 5000;
108
165
  /**
109
- * Kill an orphaned Chrome (see {@link killOrphanedChrome}) and wait until it
110
- * has exited, so its debugging port is free for the next launch.
166
+ * Kill the Chromes this session left behind and wait until they have exited,
167
+ * so their profile and debugging port are free for the next launch. The
168
+ * Chrome recorded in chrome.pid is killed (see {@link killOrphanedChrome});
169
+ * without a record (chrome.pid lost with its daemon), every Chrome carrying
170
+ * the session's marker is, unless another daemon of the session still runs.
111
171
  *
112
172
  * @returns True if a Chrome process was killed
113
173
  */
114
174
  export async function reapOrphanedChrome() {
115
175
  const chromePid = findOrphanedChrome();
116
- if (!killOrphanedChrome() || chromePid === null)
117
- return false;
176
+ const killed = chromePid !== null ? (killOrphanedChrome() ? [chromePid] : []) : killUnrecordedChromes();
118
177
  const deadline = Date.now() + ORPHAN_EXIT_WAIT_MS;
119
- while (isProcessAlive(chromePid) && Date.now() < deadline)
178
+ while (killed.some(isProcessAlive) && Date.now() < deadline)
120
179
  await delay(50);
121
- return true;
180
+ return killed.length > 0;
181
+ }
182
+ /**
183
+ * Kill the session's Chromes found by their marker (see
184
+ * {@link killSessionChromes}), unless a daemon of the session other than
185
+ * this process still runs: they could be its Chrome.
186
+ *
187
+ * @returns PIDs of the processes killed
188
+ */
189
+ function killUnrecordedChromes() {
190
+ const daemonPid = readLiveDaemonPid();
191
+ return daemonPid === null || daemonPid === process.pid ? killSessionChromes() : [];
122
192
  }
123
193
  /**
124
194
  * Read the PID of a live bdg daemon from daemon.pid.
@@ -27,7 +27,10 @@ export interface SessionCleanupResult {
27
27
  *
28
28
  * With `force`, a live daemon is killed first (after verifying its command
29
29
  * line). Then stale daemon files are removed and an orphaned bdg Chrome, if
30
- * any, is killed.
30
+ * any, is killed, and, once no daemon of the session runs, every other
31
+ * Chrome launched for this session directory (one chrome.pid lost track of).
32
+ * The record of a session that ended without `bdg stop` is removed too, and
33
+ * counts as cleaned session files.
31
34
  *
32
35
  * @param options - Cleanup options
33
36
  * @returns What was cleaned, plus warnings
@@ -2,7 +2,7 @@
2
2
  * Cleanup behind the user-facing `bdg cleanup` command.
3
3
  */
4
4
  import * as fs from 'fs';
5
- import { killOrphanedChrome, readLiveDaemonPid, removeStaleDaemonFiles, } from './staleSession.js';
5
+ import { killOrphanedChrome, killSessionChromes, readLiveDaemonPid, removeStaleDaemonFiles, } from './staleSession.js';
6
6
  import { isDaemonAlive } from '../daemonSocket.js';
7
7
  import { clearLastSessionEnd } from '../lastSession.js';
8
8
  import { SESSION_STATE_FILES, getSessionFilePath } from '../paths.js';
@@ -15,7 +15,10 @@ const DAEMON_EXIT_WAIT_MS = 2000;
15
15
  *
16
16
  * With `force`, a live daemon is killed first (after verifying its command
17
17
  * line). Then stale daemon files are removed and an orphaned bdg Chrome, if
18
- * any, is killed.
18
+ * any, is killed, and, once no daemon of the session runs, every other
19
+ * Chrome launched for this session directory (one chrome.pid lost track of).
20
+ * The record of a session that ended without `bdg stop` is removed too, and
21
+ * counts as cleaned session files.
19
22
  *
20
23
  * @param options - Cleanup options
21
24
  * @returns What was cleaned, plus warnings
@@ -25,13 +28,15 @@ export async function performSessionCleanup(options) {
25
28
  const filesBefore = countSessionFiles();
26
29
  const daemonKilled = options.force ? await killLiveDaemon(warnings) : false;
27
30
  const session = await removeStaleDaemonFiles();
28
- const chrome = killOrphanedChrome();
31
+ const orphan = killOrphanedChrome();
32
+ const unrecorded = readLiveDaemonPid() === null ? killSessionChromes() : [];
33
+ const chrome = orphan || unrecorded.length > 0;
29
34
  const output = options.removeOutput ? removeOutputFile(warnings) : false;
30
35
  const filesRemoved = countSessionFiles() < filesBefore;
31
- clearLastSessionEnd();
36
+ const endedRecordRemoved = clearLastSessionEnd();
32
37
  return {
33
38
  cleaned: {
34
- session: session || daemonKilled || filesRemoved,
39
+ session: session || daemonKilled || filesRemoved || endedRecordRemoved,
35
40
  chrome,
36
41
  daemons: daemonKilled,
37
42
  output,
@@ -22,4 +22,14 @@ export declare function probeDaemonSocket(socketPath?: string, timeoutMs?: numbe
22
22
  * @returns True if the daemon socket accepts a connection
23
23
  */
24
24
  export declare function isDaemonAlive(): Promise<boolean>;
25
+ /**
26
+ * Whether the daemon socket certainly cannot be reached: there is no socket
27
+ * file, or connecting is refused. A daemon that is only slow to accept (busy,
28
+ * a timeout) does not count, so it is never taken for a dead one.
29
+ *
30
+ * @param socketPath - Socket path (defaults to the session's daemon socket)
31
+ * @param timeoutMs - Connection timeout
32
+ * @returns True when the socket is missing or refuses connections
33
+ */
34
+ export declare function isDaemonSocketGone(socketPath?: string, timeoutMs?: number): Promise<boolean>;
25
35
  //# sourceMappingURL=daemonSocket.d.ts.map
@@ -37,4 +37,26 @@ export function probeDaemonSocket(socketPath = getSessionFilePath('DAEMON_SOCKET
37
37
  export async function isDaemonAlive() {
38
38
  return (await probeDaemonSocket()) === 'alive';
39
39
  }
40
+ /**
41
+ * Whether the daemon socket certainly cannot be reached: there is no socket
42
+ * file, or connecting is refused. A daemon that is only slow to accept (busy,
43
+ * a timeout) does not count, so it is never taken for a dead one.
44
+ *
45
+ * @param socketPath - Socket path (defaults to the session's daemon socket)
46
+ * @param timeoutMs - Connection timeout
47
+ * @returns True when the socket is missing or refuses connections
48
+ */
49
+ export function isDaemonSocketGone(socketPath = getSessionFilePath('DAEMON_SOCKET'), timeoutMs = PROBE_TIMEOUT_MS) {
50
+ return new Promise((resolve) => {
51
+ const socket = net.createConnection(socketPath);
52
+ const finish = (gone) => {
53
+ clearTimeout(timer);
54
+ socket.destroy();
55
+ resolve(gone);
56
+ };
57
+ const timer = setTimeout(() => finish(false), timeoutMs);
58
+ socket.once('connect', () => finish(false));
59
+ socket.once('error', (error) => finish(error.code === 'ENOENT' || error.code === 'ECONNREFUSED'));
60
+ });
61
+ }
40
62
  //# sourceMappingURL=daemonSocket.js.map
@@ -18,12 +18,15 @@ export interface LastSessionEnd {
18
18
  export declare function writeLastSessionEnd(reason: UnexpectedEndReason): void;
19
19
  /**
20
20
  * Forget the last session's end (a new session is starting, or `cleanup` ran).
21
+ *
22
+ * @returns True if there was a record to remove
21
23
  */
22
- export declare function clearLastSessionEnd(): void;
24
+ export declare function clearLastSessionEnd(): boolean;
23
25
  /**
24
- * How the last session ended, if it ended unexpectedly.
26
+ * How the last session of a directory ended, if it ended unexpectedly.
25
27
  *
28
+ * @param dir - Session directory (default: the selected session's)
26
29
  * @returns The record, or null
27
30
  */
28
- export declare function readLastSessionEnd(): LastSessionEnd | null;
31
+ export declare function readLastSessionEnd(dir?: string): LastSessionEnd | null;
29
32
  //# sourceMappingURL=lastSession.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * `bdg status` can explain why there is no session any more.
4
4
  */
5
5
  import * as fs from 'fs';
6
- import { getSessionFilePath } from './paths.js';
6
+ import { getSessionDir, getSessionFilePath, sessionFilePathIn } from './paths.js';
7
7
  import { createLogger } from '../ui/logging/index.js';
8
8
  import { AtomicFileWriter } from '../utils/atomicFile.js';
9
9
  import { getErrorMessage } from '../utils/errors.js';
@@ -23,18 +23,24 @@ export function writeLastSessionEnd(reason) {
23
23
  }
24
24
  /**
25
25
  * Forget the last session's end (a new session is starting, or `cleanup` ran).
26
+ *
27
+ * @returns True if there was a record to remove
26
28
  */
27
29
  export function clearLastSessionEnd() {
28
- fs.rmSync(getSessionFilePath('LAST_SESSION'), { force: true });
30
+ const file = getSessionFilePath('LAST_SESSION');
31
+ const existed = fs.existsSync(file);
32
+ fs.rmSync(file, { force: true });
33
+ return existed;
29
34
  }
30
35
  /**
31
- * How the last session ended, if it ended unexpectedly.
36
+ * How the last session of a directory ended, if it ended unexpectedly.
32
37
  *
38
+ * @param dir - Session directory (default: the selected session's)
33
39
  * @returns The record, or null
34
40
  */
35
- export function readLastSessionEnd() {
41
+ export function readLastSessionEnd(dir = getSessionDir()) {
36
42
  try {
37
- const data = JSON.parse(fs.readFileSync(getSessionFilePath('LAST_SESSION'), 'utf8'));
43
+ const data = JSON.parse(fs.readFileSync(sessionFilePathIn(dir, 'LAST_SESSION'), 'utf8'));
38
44
  return typeof data.endedAt === 'number' ? data : null;
39
45
  }
40
46
  catch (error) {
@@ -124,8 +124,10 @@ export declare function getDaemonSocketPath(): string;
124
124
  * Ensure the session directory exists.
125
125
  *
126
126
  * Creates ~/.bdg/ if it doesn't exist. Safe to call multiple times (idempotent).
127
+ * A path that cannot hold a directory is refused before `mkdir`, which
128
+ * would spin on a pseudo-filesystem ({@link makeDirectory}).
127
129
  *
128
- * @throws Error if directory creation fails due to permissions
130
+ * @throws Error if the directory cannot be created
129
131
  */
130
132
  export declare function ensureSessionDir(): void;
131
133
  export {};
@@ -9,6 +9,7 @@ import * as fs from 'fs';
9
9
  import * as os from 'os';
10
10
  import * as path from 'path';
11
11
  import { createLogger, logDebugError } from '../ui/logging/index.js';
12
+ import { makeDirectory } from '../utils/directories.js';
12
13
  const log = createLogger('session');
13
14
  /**
14
15
  * Session file paths relative to ~/.bdg/
@@ -166,13 +167,12 @@ export function getDaemonSocketPath() {
166
167
  * Ensure the session directory exists.
167
168
  *
168
169
  * Creates ~/.bdg/ if it doesn't exist. Safe to call multiple times (idempotent).
170
+ * A path that cannot hold a directory is refused before `mkdir`, which
171
+ * would spin on a pseudo-filesystem ({@link makeDirectory}).
169
172
  *
170
- * @throws Error if directory creation fails due to permissions
173
+ * @throws Error if the directory cannot be created
171
174
  */
172
175
  export function ensureSessionDir() {
173
- const dir = getSessionDir();
174
- if (!fs.existsSync(dir)) {
175
- fs.mkdirSync(dir, { recursive: true });
176
- }
176
+ makeDirectory(getSessionDir());
177
177
  }
178
178
  //# sourceMappingURL=paths.js.map
@@ -20,6 +20,7 @@ import * as path from 'path';
20
20
  import { getSessionBaseDir, getSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
21
21
  import { createLogger, logDebugError } from '../ui/logging/index.js';
22
22
  import { delay } from '../utils/async.js';
23
+ import { makeDirectory } from '../utils/directories.js';
23
24
  const log = createLogger('session');
24
25
  /** Lock file guarding port selection, in the port registry (or base session) directory */
25
26
  const PORT_LOCK_FILE = 'port.lock';
@@ -93,8 +94,8 @@ function trustedRegistryDir(create) {
93
94
  const dir = getPortRegistryDir();
94
95
  try {
95
96
  if (create) {
96
- fs.mkdirSync(path.dirname(dir), { recursive: true });
97
- fs.mkdirSync(path.join(dir, CLAIMS_DIR), { recursive: true, mode: 0o700 });
97
+ makeDirectory(path.dirname(dir));
98
+ makeDirectory(path.join(dir, CLAIMS_DIR), 0o700);
98
99
  }
99
100
  if (!fs.existsSync(dir))
100
101
  return null;
@@ -266,7 +267,7 @@ function releaseLock(lockPath, token) {
266
267
  export async function withPortLock(select) {
267
268
  const registryDir = trustedRegistryDir(true);
268
269
  const lockDir = registryDir ?? getSessionBaseDir();
269
- fs.mkdirSync(lockDir, { recursive: true });
270
+ makeDirectory(lockDir);
270
271
  const lockPath = path.join(lockDir, PORT_LOCK_FILE);
271
272
  const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
272
273
  const locked = await acquireLock(lockPath, token);