browser-debugger-cli 0.8.0 → 0.9.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 (251) hide show
  1. package/README.md +4 -1
  2. package/dist/cdp/schema.d.ts +4 -1
  3. package/dist/cdp/schema.js +48 -7
  4. package/dist/commands/cdp.js +3 -2
  5. package/dist/commands/cleanup.d.ts +11 -0
  6. package/dist/commands/cleanup.js +161 -57
  7. package/dist/commands/console.d.ts +20 -1
  8. package/dist/commands/console.js +57 -17
  9. package/dist/commands/details.js +3 -2
  10. package/dist/commands/dom/DomElementResolver.d.ts +10 -3
  11. package/dist/commands/dom/DomElementResolver.js +35 -17
  12. package/dist/commands/dom/a11y.d.ts +10 -0
  13. package/dist/commands/dom/a11y.js +27 -5
  14. package/dist/commands/dom/eval.d.ts +3 -1
  15. package/dist/commands/dom/eval.js +29 -4
  16. package/dist/commands/dom/form.js +16 -62
  17. package/dist/commands/dom/formInteraction.js +152 -113
  18. package/dist/commands/dom/formSummary.d.ts +49 -0
  19. package/dist/commands/dom/formSummary.js +180 -0
  20. package/dist/commands/dom/frames.d.ts +2 -1
  21. package/dist/commands/dom/frames.js +17 -2
  22. package/dist/commands/dom/get.d.ts +6 -5
  23. package/dist/commands/dom/get.js +92 -82
  24. package/dist/commands/dom/helpers/index.d.ts +1 -1
  25. package/dist/commands/dom/helpers/index.js +1 -1
  26. package/dist/commands/dom/helpers/query.d.ts +44 -17
  27. package/dist/commands/dom/helpers/query.js +244 -97
  28. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  29. package/dist/commands/dom/helpers/runElementCommand.js +97 -30
  30. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  31. package/dist/commands/dom/helpers/screenshot.js +164 -49
  32. package/dist/commands/dom/index.d.ts +3 -1
  33. package/dist/commands/dom/index.js +16 -6
  34. package/dist/commands/dom/layout.d.ts +14 -0
  35. package/dist/commands/dom/layout.js +54 -0
  36. package/dist/commands/dom/listeners.d.ts +5 -1
  37. package/dist/commands/dom/listeners.js +13 -3
  38. package/dist/commands/dom/query.js +2 -3
  39. package/dist/commands/dom/screenshot.d.ts +12 -2
  40. package/dist/commands/dom/screenshot.js +27 -3
  41. package/dist/commands/dom/semanticUtils.d.ts +6 -13
  42. package/dist/commands/dom/semanticUtils.js +15 -19
  43. package/dist/commands/dom/wait.d.ts +13 -0
  44. package/dist/commands/dom/wait.js +83 -0
  45. package/dist/commands/helpJson.js +2 -2
  46. package/dist/commands/network/list.js +4 -11
  47. package/dist/commands/optionBehaviors.js +112 -21
  48. package/dist/commands/page.d.ts +2 -1
  49. package/dist/commands/page.js +41 -5
  50. package/dist/commands/peek.js +4 -11
  51. package/dist/commands/sessions.d.ts +8 -0
  52. package/dist/commands/sessions.js +19 -0
  53. package/dist/commands/shared/CommandRunner.js +4 -4
  54. package/dist/commands/shared/dataFetcher.js +2 -2
  55. package/dist/commands/shared/followMode.d.ts +21 -1
  56. package/dist/commands/shared/followMode.js +29 -2
  57. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  58. package/dist/commands/shared/handleValidationError.js +12 -3
  59. package/dist/commands/shared/optionTypes.d.ts +40 -5
  60. package/dist/commands/shared/startHelpers.js +12 -3
  61. package/dist/commands/shared/validation.d.ts +3 -2
  62. package/dist/commands/shared/validation.js +4 -3
  63. package/dist/commands/start.d.ts +63 -0
  64. package/dist/commands/start.js +115 -15
  65. package/dist/commands/status.js +29 -7
  66. package/dist/commands/stop.js +7 -6
  67. package/dist/commands/tail.js +4 -11
  68. package/dist/commands/types.d.ts +2 -0
  69. package/dist/commands.js +2 -0
  70. package/dist/connection/chromeIdentity.d.ts +65 -0
  71. package/dist/connection/chromeIdentity.js +143 -0
  72. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  73. package/dist/connection/launcher/profilePreferences.js +151 -0
  74. package/dist/connection/launcher.d.ts +21 -2
  75. package/dist/connection/launcher.js +42 -16
  76. package/dist/connection/portReservation.d.ts +14 -4
  77. package/dist/connection/portReservation.js +21 -6
  78. package/dist/connection/startupExit.d.ts +8 -0
  79. package/dist/connection/startupExit.js +15 -6
  80. package/dist/constants.d.ts +6 -2
  81. package/dist/constants.js +9 -2
  82. package/dist/daemon/SessionController.js +23 -7
  83. package/dist/daemon/errors.d.ts +1 -1
  84. package/dist/daemon/errors.js +1 -1
  85. package/dist/daemon/launcher.d.ts +2 -1
  86. package/dist/daemon/launcher.js +5 -6
  87. package/dist/daemon/server/SocketServer.js +1 -2
  88. package/dist/daemon/session/Session.d.ts +13 -0
  89. package/dist/daemon/session/Session.js +57 -8
  90. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  91. package/dist/daemon/session/chromeConnection.js +45 -8
  92. package/dist/daemon/session/commandRegistry.js +52 -62
  93. package/dist/daemon/session/interactions.d.ts +35 -9
  94. package/dist/daemon/session/interactions.js +36 -9
  95. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  96. package/dist/daemon/session/triggeredRequests.js +157 -0
  97. package/dist/daemon/session/types.d.ts +5 -1
  98. package/dist/daemon.js +5393 -1600
  99. package/dist/errors/messages.d.ts +387 -24
  100. package/dist/errors/messages.js +761 -67
  101. package/dist/index.js +3976 -1558
  102. package/dist/ipc/client.d.ts +12 -1
  103. package/dist/ipc/client.js +22 -3
  104. package/dist/ipc/protocol/commands.d.ts +89 -4
  105. package/dist/ipc/protocol/commands.js +2 -0
  106. package/dist/ipc/protocol/domTypes.d.ts +258 -7
  107. package/dist/ipc/session/lifecycle.d.ts +8 -1
  108. package/dist/ipc/session/queries.d.ts +5 -1
  109. package/dist/ipc/session/types.d.ts +5 -0
  110. package/dist/ipc/transport/index.d.ts +2 -1
  111. package/dist/ipc/transport/index.js +2 -2
  112. package/dist/runtime/dom/actionEffects.d.ts +106 -0
  113. package/dist/runtime/dom/actionEffects.js +256 -0
  114. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -0
  115. package/dist/runtime/dom/actionEffectsScripts.js +234 -0
  116. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  117. package/dist/runtime/dom/elementGeometry.js +553 -0
  118. package/dist/runtime/dom/elementInfo.d.ts +77 -0
  119. package/dist/runtime/dom/elementInfo.js +191 -0
  120. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  121. package/dist/runtime/dom/evalHelpers.js +136 -26
  122. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  123. package/dist/runtime/dom/eventListeners.js +174 -47
  124. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  125. package/dist/runtime/dom/formDiscovery.js +116 -16
  126. package/dist/runtime/dom/formFillHelpers/fill.d.ts +10 -0
  127. package/dist/runtime/dom/formFillHelpers/fill.js +125 -10
  128. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  129. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  130. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  131. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  132. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  133. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  134. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  135. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  136. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  137. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  138. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  139. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  140. package/dist/runtime/dom/frameLayout.js +140 -0
  141. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  142. package/dist/runtime/dom/frameOrigin.js +62 -0
  143. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  144. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  145. package/dist/runtime/dom/frameSelection.d.ts +1 -1
  146. package/dist/runtime/dom/frameSelection.js +2 -2
  147. package/dist/runtime/dom/frames.d.ts +25 -2
  148. package/dist/runtime/dom/frames.js +202 -63
  149. package/dist/runtime/dom/layout.d.ts +67 -0
  150. package/dist/runtime/dom/layout.js +333 -0
  151. package/dist/runtime/dom/listenerPageScripts.d.ts +66 -0
  152. package/dist/runtime/dom/listenerPageScripts.js +279 -0
  153. package/dist/runtime/dom/listenerSummary.d.ts +132 -11
  154. package/dist/runtime/dom/listenerSummary.js +344 -22
  155. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  156. package/dist/runtime/dom/pageActivity.js +123 -0
  157. package/dist/runtime/dom/reactEventHelpers.d.ts +58 -2
  158. package/dist/runtime/dom/reactEventHelpers.js +212 -41
  159. package/dist/runtime/dom/targetNode.d.ts +80 -27
  160. package/dist/runtime/dom/targetNode.js +249 -33
  161. package/dist/runtime/dom/wait.d.ts +25 -0
  162. package/dist/runtime/dom/wait.js +199 -0
  163. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  164. package/dist/runtime/dom/waitCondition.js +75 -0
  165. package/dist/runtime/page/emulation.d.ts +51 -0
  166. package/dist/runtime/page/emulation.js +80 -0
  167. package/dist/runtime/page/loadingState.d.ts +36 -0
  168. package/dist/runtime/page/loadingState.js +86 -0
  169. package/dist/runtime/page/navigation.d.ts +46 -2
  170. package/dist/runtime/page/navigation.js +69 -33
  171. package/dist/session/QueryCacheManager.d.ts +11 -1
  172. package/dist/session/QueryCacheManager.js +25 -3
  173. package/dist/session/chromeOwners.d.ts +34 -0
  174. package/dist/session/chromeOwners.js +51 -0
  175. package/dist/session/cleanup/staleSession.d.ts +11 -1
  176. package/dist/session/cleanup/staleSession.js +17 -6
  177. package/dist/session/cleanup/userCommands.js +2 -4
  178. package/dist/session/metadata.d.ts +5 -1
  179. package/dist/session/metadata.js +2 -1
  180. package/dist/session/paths.d.ts +77 -3
  181. package/dist/session/paths.js +111 -5
  182. package/dist/session/port.d.ts +31 -7
  183. package/dist/session/port.js +50 -43
  184. package/dist/session/portClaims.d.ts +66 -0
  185. package/dist/session/portClaims.js +284 -0
  186. package/dist/session/sessionList.d.ts +58 -0
  187. package/dist/session/sessionList.js +199 -0
  188. package/dist/session/sessionName.d.ts +46 -0
  189. package/dist/session/sessionName.js +97 -0
  190. package/dist/telemetry/a11y.d.ts +8 -3
  191. package/dist/telemetry/a11y.js +92 -28
  192. package/dist/telemetry/requestKinds.d.ts +32 -0
  193. package/dist/telemetry/requestKinds.js +61 -0
  194. package/dist/telemetry/requestState.d.ts +31 -0
  195. package/dist/telemetry/requestState.js +38 -0
  196. package/dist/types.d.ts +80 -3
  197. package/dist/ui/formatters/a11y.js +3 -0
  198. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  199. package/dist/ui/formatters/console/chronological.js +17 -4
  200. package/dist/ui/formatters/console/json.js +3 -4
  201. package/dist/ui/formatters/console/shared.d.ts +12 -0
  202. package/dist/ui/formatters/console.d.ts +2 -2
  203. package/dist/ui/formatters/console.js +1 -1
  204. package/dist/ui/formatters/details.js +2 -1
  205. package/dist/ui/formatters/dom.d.ts +26 -14
  206. package/dist/ui/formatters/dom.js +65 -52
  207. package/dist/ui/formatters/form.js +29 -18
  208. package/dist/ui/formatters/layout.d.ts +31 -0
  209. package/dist/ui/formatters/layout.js +53 -0
  210. package/dist/ui/formatters/listeners.d.ts +3 -2
  211. package/dist/ui/formatters/listeners.js +73 -9
  212. package/dist/ui/formatters/networkHeaders.js +13 -0
  213. package/dist/ui/formatters/preview.js +2 -1
  214. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  215. package/dist/ui/formatters/requestStatus.js +2 -30
  216. package/dist/ui/formatters/sessions.d.ts +12 -0
  217. package/dist/ui/formatters/sessions.js +40 -0
  218. package/dist/ui/formatters/status.d.ts +21 -2
  219. package/dist/ui/formatters/status.js +47 -10
  220. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  221. package/dist/ui/formatters/triggeredRequests.js +65 -0
  222. package/dist/ui/formatting.d.ts +10 -0
  223. package/dist/ui/formatting.js +28 -36
  224. package/dist/ui/messages/chrome.d.ts +9 -0
  225. package/dist/ui/messages/chrome.js +17 -5
  226. package/dist/ui/messages/commands.d.ts +388 -14
  227. package/dist/ui/messages/commands.js +664 -21
  228. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  229. package/dist/ui/messages/consoleMessages.js +17 -0
  230. package/dist/ui/messages/hints.js +2 -1
  231. package/dist/ui/messages/preview.js +5 -4
  232. package/dist/ui/messages/session.d.ts +16 -21
  233. package/dist/ui/messages/session.js +28 -26
  234. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  235. package/dist/ui/messages/sessionCommand.js +52 -0
  236. package/dist/utils/async.d.ts +8 -0
  237. package/dist/utils/async.js +19 -0
  238. package/dist/utils/http.d.ts +22 -1
  239. package/dist/utils/http.js +28 -9
  240. package/dist/utils/selectorFilters.d.ts +36 -8
  241. package/dist/utils/selectorFilters.js +267 -53
  242. package/dist/utils/shellDetection.d.ts +8 -2
  243. package/dist/utils/shellDetection.js +120 -33
  244. package/dist/utils/suggestions.d.ts +26 -0
  245. package/dist/utils/suggestions.js +73 -0
  246. package/dist/utils/taskMappings.js +10 -0
  247. package/dist/utils/url.d.ts +12 -2
  248. package/dist/utils/url.js +69 -7
  249. package/package.json +1 -1
  250. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  251. package/dist/ui/formatters/sessionFormatters.js +0 -121
@@ -3,14 +3,15 @@
3
3
  *
4
4
  * Handles automatic port selection and persistence for session isolation.
5
5
  * Each session directory can have its own persistent port, enabling multiple
6
- * concurrent bdg sessions with different BDG_SESSION_DIR values.
6
+ * concurrent bdg sessions (named sessions or different BDG_SESSION_DIR values).
7
7
  */
8
8
  import * as fs from 'fs';
9
9
  import * as net from 'net';
10
10
  import { isPortAnswering } from '../connection/portReservation.js';
11
11
  import { DEFAULT_CDP_PORT } from '../constants.js';
12
12
  import { CommandError } from '../errors/index.js';
13
- import { ensureSessionDir, getSessionFilePath } from './paths.js';
13
+ import { ensureSessionDir, getSessionFilePath, getSessionName } from './paths.js';
14
+ import { portsClaimedByOtherSessions, readPortFile, withPortLock } from './portClaims.js';
14
15
  import { EXIT_CODES } from '../utils/exitCodes.js';
15
16
  /**
16
17
  * Port range for automatic selection.
@@ -20,7 +21,7 @@ const PORT_RANGE_START = DEFAULT_CDP_PORT;
20
21
  const PORT_RANGE_END = 9322; // Allow 100 ports for concurrent sessions
21
22
  /**
22
23
  * Check if a port is available (not in use by any process, including one
23
- * listening on all interfaces).
24
+ * listening on all interfaces or on ::1 only).
24
25
  *
25
26
  * @param port - Port number to check
26
27
  * @returns Promise resolving to true if port is available
@@ -40,20 +41,31 @@ async function isPortAvailable(port) {
40
41
  });
41
42
  });
42
43
  }
44
+ /**
45
+ * First port tried for this session: the default session starts at
46
+ * DEFAULT_CDP_PORT, named sessions one above so they leave it to the default
47
+ * session.
48
+ *
49
+ * @returns First candidate port
50
+ */
51
+ export function firstCandidatePort() {
52
+ return getSessionName() === null ? PORT_RANGE_START : PORT_RANGE_START + 1;
53
+ }
43
54
  /**
44
55
  * Find an available port starting from a given port.
45
56
  *
46
57
  * @param startPort - Port to start scanning from
58
+ * @param claimed - Ports to skip (claimed by other sessions)
47
59
  * @returns Promise resolving to an available port
48
- * @throws Error if no available port found in range
60
+ * @throws CommandError if no available port found in range
49
61
  */
50
- async function findAvailablePort(startPort = PORT_RANGE_START) {
62
+ export async function findAvailablePort(startPort = PORT_RANGE_START, claimed = new Set()) {
51
63
  for (let port = startPort; port <= PORT_RANGE_END; port++) {
52
- if (await isPortAvailable(port)) {
64
+ if (!claimed.has(port) && (await isPortAvailable(port))) {
53
65
  return port;
54
66
  }
55
67
  }
56
- throw new CommandError(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}.`, { suggestion: 'Stop some bdg sessions or Chrome instances and retry.' }, EXIT_CODES.SOFTWARE_ERROR);
68
+ throw new CommandError(`No available port found in range ${startPort}-${PORT_RANGE_END}.`, { suggestion: 'Stop some bdg sessions or Chrome instances and retry.' }, EXIT_CODES.SOFTWARE_ERROR);
57
69
  }
58
70
  /**
59
71
  * Read the saved port from the session directory.
@@ -61,21 +73,8 @@ async function findAvailablePort(startPort = PORT_RANGE_START) {
61
73
  * @returns Saved port number or null if not found/invalid
62
74
  */
63
75
  export function readSessionPort() {
64
- try {
65
- const portPath = getSessionFilePath('PORT');
66
- if (!fs.existsSync(portPath)) {
67
- return null;
68
- }
69
- const content = fs.readFileSync(portPath, 'utf-8').trim();
70
- const port = parseInt(content, 10);
71
- if (isNaN(port) || port < 1 || port > 65535) {
72
- return null;
73
- }
74
- return port;
75
- }
76
- catch {
77
- return null;
78
- }
76
+ const portPath = getSessionFilePath('PORT');
77
+ return fs.existsSync(portPath) ? readPortFile(portPath) : null;
79
78
  }
80
79
  /**
81
80
  * Save the port to the session directory.
@@ -91,34 +90,42 @@ export function writeSessionPort(port) {
91
90
  * Get or allocate a port for this session.
92
91
  *
93
92
  * Logic:
94
- * 1. If a port is explicitly provided, use it (user override)
95
- * 2. Check if there's a saved port in the session directory
96
- * 3. If saved port exists and is available, reuse it (session stability)
97
- * 4. Otherwise, find a new available port and save it
93
+ * 1. If a port is explicitly provided, use it (user override; a named session
94
+ * saves and claims it under the lock so other sessions skip it)
95
+ * 2. Otherwise, under a lock shared by all sessions on the machine (any
96
+ * BDG_SESSION_DIR), reuse the saved port if it is free and no other
97
+ * running session claims it (session stability)
98
+ * 3. Otherwise, take the first free, unclaimed port from
99
+ * {@link firstCandidatePort} upwards and save it (the claim)
98
100
  *
99
- * This provides session isolation: different BDG_SESSION_DIR values
100
- * will automatically use different ports.
101
+ * The claim is also recorded in the machine-wide registry (under the lock),
102
+ * so sessions of other base directories skip it too.
103
+ *
104
+ * This provides session isolation: named sessions and different
105
+ * BDG_SESSION_DIR values automatically use different ports, even when they
106
+ * start at the same time.
101
107
  *
102
108
  * @param explicitPort - User-provided port (takes precedence)
103
109
  * @returns Promise resolving to the port to use
104
110
  */
105
111
  export async function getSessionPort(explicitPort) {
106
- // User explicitly specified a port - use it directly
107
112
  if (explicitPort !== undefined && explicitPort !== null) {
108
- return explicitPort;
109
- }
110
- // Check for saved session port
111
- const savedPort = readSessionPort();
112
- if (savedPort !== null) {
113
- // Verify the saved port is still available
114
- if (await isPortAvailable(savedPort)) {
115
- return savedPort;
116
- }
117
- // Saved port is in use by another process, need to find a new one
113
+ if (getSessionName() === null)
114
+ return explicitPort;
115
+ return withPortLock((recordClaim) => {
116
+ writeSessionPort(explicitPort);
117
+ recordClaim(explicitPort);
118
+ return Promise.resolve(explicitPort);
119
+ });
118
120
  }
119
- // Find a new available port
120
- const newPort = await findAvailablePort();
121
- writeSessionPort(newPort);
122
- return newPort;
121
+ return withPortLock(async (recordClaim) => {
122
+ const claimed = portsClaimedByOtherSessions();
123
+ const savedPort = readSessionPort();
124
+ const reuse = savedPort !== null && !claimed.has(savedPort) && (await isPortAvailable(savedPort));
125
+ const port = reuse ? savedPort : await findAvailablePort(firstCandidatePort(), claimed);
126
+ writeSessionPort(port);
127
+ recordClaim(port);
128
+ return port;
129
+ });
123
130
  }
124
131
  //# sourceMappingURL=port.js.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Port claims across concurrent sessions.
3
+ *
4
+ * Sessions started at the same time must not pick the same CDP port: Chrome
5
+ * binds it only after the choice is made. A session claims its port by
6
+ * writing `port.txt` while holding a lock; a running session's claim is
7
+ * skipped by the others.
8
+ *
9
+ * Ports belong to the machine, not to a session directory, so the lock and a
10
+ * registry of claiming session directories live in one directory per user
11
+ * under the OS temp directory: sessions of different `BDG_SESSION_DIR`s see
12
+ * each other's claims too. That directory is used only if it is a real
13
+ * directory owned by the user that nobody else can write to; otherwise the
14
+ * lock falls back to the base session directory and only that directory's
15
+ * claims are seen (the launched Chrome's identity is still checked).
16
+ */
17
+ /** Records a port claim; a no-op when the registry cannot be used safely */
18
+ export type RecordPortClaim = (port: number) => void;
19
+ /**
20
+ * Read a port number from a `port.txt` file.
21
+ *
22
+ * @param portPath - File path
23
+ * @returns Port, or null if missing or invalid
24
+ */
25
+ export declare function readPortFile(portPath: string): number | null;
26
+ /**
27
+ * Directory holding the port lock and the claims registry, shared by every
28
+ * session of the user on this machine.
29
+ *
30
+ * @returns `$BDG_PORT_REGISTRY_DIR`, else `<os temp dir>/bdg-ports-<uid>`
31
+ */
32
+ export declare function getPortRegistryDir(): string;
33
+ /**
34
+ * Whether a path is a directory the current user can trust: a real directory
35
+ * (not a symlink), owned by the user, not writable by group or others.
36
+ *
37
+ * @param dir - Directory
38
+ * @returns Why it cannot be trusted, or null if it can
39
+ */
40
+ export declare function untrustedDirReason(dir: string): string | null;
41
+ /**
42
+ * Session directories other than the selected one that bdg knows of: those of
43
+ * this base directory, and those of any base directory found in the
44
+ * machine-wide registry. They need not hold a running session.
45
+ *
46
+ * @returns Session directories
47
+ */
48
+ export declare function otherSessionDirs(): string[];
49
+ /**
50
+ * Ports claimed by other sessions that are running or starting (their daemon
51
+ * socket exists), see {@link otherSessionDirs}.
52
+ *
53
+ * @returns Claimed ports
54
+ */
55
+ export declare function portsClaimedByOtherSessions(): Set<number>;
56
+ /**
57
+ * Run port selection under the lock shared by all sessions of the user on
58
+ * this machine (in the trusted registry directory; else in the base session
59
+ * directory). If the lock cannot be taken in time, the selection runs anyway.
60
+ *
61
+ * @param select - Chooses the port; gets a function recording the claim in
62
+ * the registry, a no-op without the registry lock
63
+ * @returns The chosen port
64
+ */
65
+ export declare function withPortLock(select: (recordClaim: RecordPortClaim) => Promise<number>): Promise<number>;
66
+ //# sourceMappingURL=portClaims.d.ts.map
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Port claims across concurrent sessions.
3
+ *
4
+ * Sessions started at the same time must not pick the same CDP port: Chrome
5
+ * binds it only after the choice is made. A session claims its port by
6
+ * writing `port.txt` while holding a lock; a running session's claim is
7
+ * skipped by the others.
8
+ *
9
+ * Ports belong to the machine, not to a session directory, so the lock and a
10
+ * registry of claiming session directories live in one directory per user
11
+ * under the OS temp directory: sessions of different `BDG_SESSION_DIR`s see
12
+ * each other's claims too. That directory is used only if it is a real
13
+ * directory owned by the user that nobody else can write to; otherwise the
14
+ * lock falls back to the base session directory and only that directory's
15
+ * claims are seen (the launched Chrome's identity is still checked).
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ import { getSessionBaseDir, getSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
21
+ import { createLogger, logDebugError } from '../ui/logging/index.js';
22
+ import { delay } from '../utils/async.js';
23
+ const log = createLogger('session');
24
+ /** Lock file guarding port selection, in the port registry (or base session) directory */
25
+ const PORT_LOCK_FILE = 'port.lock';
26
+ /** Subdirectory of the port registry: one file per claimed port, holding the claiming session directory */
27
+ const CLAIMS_DIR = 'claims';
28
+ /** Overrides the port registry directory (tests) */
29
+ const PORT_REGISTRY_DIR_ENV = 'BDG_PORT_REGISTRY_DIR';
30
+ /** How long to wait for the lock before choosing a port without it */
31
+ const LOCK_WAIT_MS = 5000;
32
+ /** Lock age after which its holder is assumed dead */
33
+ const STALE_LOCK_MS = 10000;
34
+ const LOCK_POLL_MS = 25;
35
+ /** Permission bits that let other users write */
36
+ const GROUP_OTHER_WRITE = 0o022;
37
+ /**
38
+ * Read a port number from a `port.txt` file.
39
+ *
40
+ * @param portPath - File path
41
+ * @returns Port, or null if missing or invalid
42
+ */
43
+ export function readPortFile(portPath) {
44
+ try {
45
+ const port = parseInt(fs.readFileSync(portPath, 'utf-8').trim(), 10);
46
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
47
+ }
48
+ catch (error) {
49
+ logDebugError(log, `read ${portPath}`, error);
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Directory holding the port lock and the claims registry, shared by every
55
+ * session of the user on this machine.
56
+ *
57
+ * @returns `$BDG_PORT_REGISTRY_DIR`, else `<os temp dir>/bdg-ports-<uid>`
58
+ */
59
+ export function getPortRegistryDir() {
60
+ const override = process.env[PORT_REGISTRY_DIR_ENV]?.trim();
61
+ if (override)
62
+ return path.resolve(override);
63
+ const uid = process.getuid?.();
64
+ return path.join(os.tmpdir(), uid === undefined ? 'bdg-ports' : `bdg-ports-${uid}`);
65
+ }
66
+ /**
67
+ * Whether a path is a directory the current user can trust: a real directory
68
+ * (not a symlink), owned by the user, not writable by group or others.
69
+ *
70
+ * @param dir - Directory
71
+ * @returns Why it cannot be trusted, or null if it can
72
+ */
73
+ export function untrustedDirReason(dir) {
74
+ const stat = fs.lstatSync(dir);
75
+ if (!stat.isDirectory())
76
+ return 'not a directory';
77
+ const uid = process.getuid?.();
78
+ if (uid !== undefined && stat.uid !== uid)
79
+ return `owned by uid ${stat.uid}`;
80
+ if (process.platform !== 'win32' && (stat.mode & GROUP_OTHER_WRITE) !== 0) {
81
+ return `writable by others (mode ${(stat.mode & 0o777).toString(8)})`;
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * The port registry directory, created (mode 0700) if missing, if it can be
87
+ * trusted (see {@link untrustedDirReason}).
88
+ *
89
+ * @param create - Create the directory (and its claims subdirectory) if missing
90
+ * @returns The directory, or null if it is missing or untrusted
91
+ */
92
+ function trustedRegistryDir(create) {
93
+ const dir = getPortRegistryDir();
94
+ try {
95
+ if (create) {
96
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
97
+ fs.mkdirSync(path.join(dir, CLAIMS_DIR), { recursive: true, mode: 0o700 });
98
+ }
99
+ if (!fs.existsSync(dir))
100
+ return null;
101
+ const reason = untrustedDirReason(dir) ?? untrustedDirReason(path.join(dir, CLAIMS_DIR));
102
+ if (reason === null)
103
+ return dir;
104
+ log.info(`Not using the port registry ${dir} (${reason}); port claims are per directory`);
105
+ }
106
+ catch (error) {
107
+ logDebugError(log, `use the port registry ${dir}`, error);
108
+ }
109
+ return null;
110
+ }
111
+ /**
112
+ * Record in the registry that this session directory claims a port. Written
113
+ * to a new temporary file that is renamed into place, so no symlink is
114
+ * followed. The claim holds while that session runs and its `port.txt` names
115
+ * the port; it needs no removal.
116
+ *
117
+ * @param registryDir - Trusted registry directory
118
+ * @param port - Claimed port
119
+ */
120
+ function writePortClaim(registryDir, port) {
121
+ const claimPath = path.join(registryDir, CLAIMS_DIR, String(port));
122
+ const tempPath = `${claimPath}.${process.pid}.tmp`;
123
+ try {
124
+ fs.writeFileSync(tempPath, getSessionDir(), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
125
+ fs.renameSync(tempPath, claimPath);
126
+ }
127
+ catch (error) {
128
+ logDebugError(log, `record the claim of port ${port}`, error);
129
+ fs.rmSync(tempPath, { force: true });
130
+ }
131
+ }
132
+ /**
133
+ * Session directories in the registry (any base directory).
134
+ *
135
+ * @returns Claiming session directories; none if the registry is untrusted
136
+ */
137
+ function registeredSessionDirs() {
138
+ const registryDir = trustedRegistryDir(false);
139
+ if (registryDir === null)
140
+ return [];
141
+ const claimsDir = path.join(registryDir, CLAIMS_DIR);
142
+ try {
143
+ return fs
144
+ .readdirSync(claimsDir)
145
+ .filter((file) => /^\d+$/.test(file))
146
+ .map((file) => readClaim(path.join(claimsDir, file)))
147
+ .filter((dir) => dir !== null);
148
+ }
149
+ catch (error) {
150
+ logDebugError(log, `list ${claimsDir}`, error);
151
+ return [];
152
+ }
153
+ }
154
+ /**
155
+ * Read the session directory of one claim.
156
+ *
157
+ * @param claimPath - Claim file
158
+ * @returns Absolute session directory, or null if unreadable or partly written
159
+ */
160
+ function readClaim(claimPath) {
161
+ try {
162
+ const dir = fs.readFileSync(claimPath, 'utf8').trim();
163
+ return path.isAbsolute(dir) ? dir : null;
164
+ }
165
+ catch (error) {
166
+ logDebugError(log, `read ${claimPath}`, error);
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Session directories other than the selected one that bdg knows of: those of
172
+ * this base directory, and those of any base directory found in the
173
+ * machine-wide registry. They need not hold a running session.
174
+ *
175
+ * @returns Session directories
176
+ */
177
+ export function otherSessionDirs() {
178
+ const ownDir = getSessionDir();
179
+ const dirs = new Set([...listSessionDirs().map(({ dir }) => dir), ...registeredSessionDirs()]);
180
+ return [...dirs].filter((dir) => dir !== ownDir);
181
+ }
182
+ /**
183
+ * Ports claimed by other sessions that are running or starting (their daemon
184
+ * socket exists), see {@link otherSessionDirs}.
185
+ *
186
+ * @returns Claimed ports
187
+ */
188
+ export function portsClaimedByOtherSessions() {
189
+ const ports = otherSessionDirs()
190
+ .filter((dir) => fs.existsSync(sessionFilePathIn(dir, 'DAEMON_SOCKET')))
191
+ .map((dir) => readPortFile(sessionFilePathIn(dir, 'PORT')));
192
+ return new Set(ports.filter((port) => port !== null));
193
+ }
194
+ /**
195
+ * Take the port-selection lock.
196
+ *
197
+ * @param lockPath - Lock file path
198
+ * @param token - Written into the lock so only its owner removes it
199
+ * @returns True if the lock was taken; false after {@link LOCK_WAIT_MS} or
200
+ * when the lock file cannot be created
201
+ */
202
+ async function acquireLock(lockPath, token) {
203
+ const deadline = Date.now() + LOCK_WAIT_MS;
204
+ while (Date.now() < deadline) {
205
+ try {
206
+ fs.writeFileSync(lockPath, token, { flag: 'wx' });
207
+ return true;
208
+ }
209
+ catch (error) {
210
+ if (error.code !== 'EEXIST') {
211
+ logDebugError(log, 'take the port lock', error);
212
+ return false;
213
+ }
214
+ removeStaleLock(lockPath);
215
+ await delay(LOCK_POLL_MS);
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+ /**
221
+ * Remove a lock left by a process that died while holding it. The lock is
222
+ * moved aside first (only one waiter's rename succeeds); if another process
223
+ * took a fresh lock in the meantime, that one is put back.
224
+ *
225
+ * @param lockPath - Lock file path
226
+ */
227
+ function removeStaleLock(lockPath) {
228
+ try {
229
+ if (Date.now() - fs.statSync(lockPath).mtimeMs <= STALE_LOCK_MS)
230
+ return;
231
+ const staleOwner = fs.readFileSync(lockPath, 'utf8');
232
+ const moved = `${lockPath}.${process.pid}.stale`;
233
+ fs.renameSync(lockPath, moved);
234
+ if (fs.readFileSync(moved, 'utf8') !== staleOwner)
235
+ fs.linkSync(moved, lockPath);
236
+ fs.rmSync(moved, { force: true });
237
+ }
238
+ catch (error) {
239
+ logDebugError(log, 'check the port lock', error);
240
+ }
241
+ }
242
+ /**
243
+ * Release the lock if it is still ours (it may have been taken over as stale).
244
+ *
245
+ * @param lockPath - Lock file path
246
+ * @param token - The token written when the lock was taken
247
+ */
248
+ function releaseLock(lockPath, token) {
249
+ try {
250
+ if (fs.readFileSync(lockPath, 'utf8') === token)
251
+ fs.rmSync(lockPath, { force: true });
252
+ }
253
+ catch (error) {
254
+ logDebugError(log, 'release the port lock', error);
255
+ }
256
+ }
257
+ /**
258
+ * Run port selection under the lock shared by all sessions of the user on
259
+ * this machine (in the trusted registry directory; else in the base session
260
+ * directory). If the lock cannot be taken in time, the selection runs anyway.
261
+ *
262
+ * @param select - Chooses the port; gets a function recording the claim in
263
+ * the registry, a no-op without the registry lock
264
+ * @returns The chosen port
265
+ */
266
+ export async function withPortLock(select) {
267
+ const registryDir = trustedRegistryDir(true);
268
+ const lockDir = registryDir ?? getSessionBaseDir();
269
+ fs.mkdirSync(lockDir, { recursive: true });
270
+ const lockPath = path.join(lockDir, PORT_LOCK_FILE);
271
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
272
+ const locked = await acquireLock(lockPath, token);
273
+ if (!locked)
274
+ log.debug('Port lock busy; choosing a port without it');
275
+ const recordClaim = locked && registryDir !== null ? (port) => writePortClaim(registryDir, port) : () => { };
276
+ try {
277
+ return await select(recordClaim);
278
+ }
279
+ finally {
280
+ if (locked)
281
+ releaseLock(lockPath, token);
282
+ }
283
+ }
284
+ //# sourceMappingURL=portClaims.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Sessions across the default and named session directories (`bdg sessions`):
3
+ * running ones, and ones whose daemon died and left a Chrome or files behind.
4
+ */
5
+ import type { StatusResponseData } from '../ipc/session/queries.js';
6
+ import { type SocketProbeResult } from './daemonSocket.js';
7
+ import { type SessionDirEntry } from './paths.js';
8
+ /**
9
+ * State of a session: its daemon runs (`active`, `starting`, `ending`,
10
+ * `unresponsive`), or died and left its Chrome running (`crashed`) or only
11
+ * files (`stale`).
12
+ */
13
+ export type RunningSessionState = 'active' | 'starting' | 'ending' | 'unresponsive' | 'crashed' | 'stale';
14
+ /**
15
+ * One session in the list.
16
+ */
17
+ export interface RunningSessionInfo {
18
+ /** Session name, or null for the default session */
19
+ name: string | null;
20
+ state: RunningSessionState;
21
+ /** Current page URL (or the URL being opened while starting) */
22
+ url?: string;
23
+ /** CDP port */
24
+ port?: number;
25
+ daemonPid?: number;
26
+ /** Chrome launched by bdg (absent for an attached Chrome) */
27
+ chromePid?: number;
28
+ /** Command that cleans up a crashed or stale session */
29
+ cleanup?: string;
30
+ }
31
+ /**
32
+ * Summarize a daemon's status response.
33
+ *
34
+ * @param name - Session name
35
+ * @param data - Status data
36
+ * @returns Session info
37
+ */
38
+ export declare function toRunningSession(name: string | null, data: StatusResponseData): RunningSessionInfo;
39
+ /**
40
+ * A session whose daemon is gone: `crashed` while the Chrome bdg launched for
41
+ * it still runs, `stale` when only its files are left. Nothing is changed on
42
+ * disk; `cleanup` names the command that removes them.
43
+ *
44
+ * @param entry - Session directory
45
+ * @param probe - Result of probing its daemon socket
46
+ * @returns Session info, or null when the directory holds no session state
47
+ */
48
+ export declare function describeLeftovers({ name, dir }: SessionDirEntry, probe: SocketProbeResult): RunningSessionInfo | null;
49
+ /**
50
+ * Every session: the default one first, then named sessions by name. Includes
51
+ * crashed and stale sessions, and directories `--session` cannot reach, so
52
+ * their leftovers can be cleaned up. A directory differing only in case
53
+ * (`ALPHA`) that `--session alpha` reaches is listed once, as `alpha`.
54
+ *
55
+ * @returns Sessions
56
+ */
57
+ export declare function listRunningSessions(): Promise<RunningSessionInfo[]>;
58
+ //# sourceMappingURL=sessionList.d.ts.map