@narumitw/pi-chrome-devtools 0.53.1 → 0.53.3

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 (41) hide show
  1. package/README.md +89 -230
  2. package/dist/chunks/{browser-settings-menu-REIU7C3Q.js → browser-settings-menu-SR7KLZT6.js} +16 -42
  3. package/dist/chunks/browser-settings-menu-SR7KLZT6.js.map +7 -0
  4. package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4KMN4SFF.js} +2 -2
  5. package/dist/chunks/chunk-4KMN4SFF.js.map +7 -0
  6. package/dist/chunks/{chunk-SRT3UUIP.js → chunk-F5VG35UI.js} +23 -74
  7. package/dist/chunks/chunk-F5VG35UI.js.map +7 -0
  8. package/dist/chunks/{chunk-YJVYJ2L2.js → chunk-MZCGM6D3.js} +43 -48
  9. package/dist/chunks/chunk-MZCGM6D3.js.map +7 -0
  10. package/dist/chunks/{menu-H5HMVGHI.js → menu-YR4VRT7D.js} +10 -23
  11. package/dist/chunks/menu-YR4VRT7D.js.map +7 -0
  12. package/dist/chunks/{tools-6P7EUX37.js → tools-6R7Z6HWJ.js} +17 -59
  13. package/dist/chunks/tools-6R7Z6HWJ.js.map +7 -0
  14. package/dist/index.ts +23 -48
  15. package/dist/index.ts.map +2 -2
  16. package/docs/browser-setup.md +147 -0
  17. package/package.json +54 -55
  18. package/src/browser-manager.ts +872 -942
  19. package/src/browser-settings-menu.ts +315 -350
  20. package/src/cdp-client.ts +381 -398
  21. package/src/chrome-devtools.ts +205 -232
  22. package/src/display.ts +35 -0
  23. package/src/lazy-tools.ts +158 -181
  24. package/src/menu.ts +409 -446
  25. package/src/render.ts +74 -84
  26. package/src/runtime.ts +137 -140
  27. package/src/screenshot.ts +170 -186
  28. package/src/settings.ts +472 -523
  29. package/src/tool-names.ts +8 -14
  30. package/src/tool-selector.ts +245 -289
  31. package/src/tools.ts +218 -226
  32. package/src/webmcp/discovery.ts +184 -188
  33. package/src/webmcp/policy.ts +296 -329
  34. package/src/webmcp/protocol.ts +324 -337
  35. package/src/webmcp/tools.ts +136 -147
  36. package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +0 -7
  37. package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
  38. package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
  39. package/dist/chunks/chunk-YJVYJ2L2.js.map +0 -7
  40. package/dist/chunks/menu-H5HMVGHI.js.map +0 -7
  41. package/dist/chunks/tools-6P7EUX37.js.map +0 -7
@@ -5,1218 +5,1148 @@ import { createServer } from "node:net";
5
5
  import { tmpdir } from "node:os";
6
6
  import { delimiter, isAbsolute, join, resolve } from "node:path";
7
7
  import {
8
- BROWSER_SHUTDOWN_WAIT_MS,
9
- type BrowserCandidate,
10
- type BrowserCandidateDefinition,
11
- type BrowserLaunchAttempt,
12
- DEFAULT_ENDPOINT_RETRY_MS,
13
- DEFAULT_ENDPOINT_WAIT_MS,
14
- DEFAULT_HTTP_TIMEOUT_MS,
15
- DEVTOOLS_ACTIVE_PORT_FILE,
16
- type DevToolsPage,
17
- invalidateWebMcpOperations,
18
- MANAGED_BROWSER_PROFILE_PREFIX,
19
- type ManagedBrowser,
20
- state,
8
+ BROWSER_SHUTDOWN_WAIT_MS,
9
+ type BrowserCandidate,
10
+ type BrowserCandidateDefinition,
11
+ type BrowserLaunchAttempt,
12
+ DEFAULT_ENDPOINT_RETRY_MS,
13
+ DEFAULT_ENDPOINT_WAIT_MS,
14
+ DEFAULT_HTTP_TIMEOUT_MS,
15
+ DEVTOOLS_ACTIVE_PORT_FILE,
16
+ type DevToolsPage,
17
+ invalidateWebMcpOperations,
18
+ MANAGED_BROWSER_PROFILE_PREFIX,
19
+ type ManagedBrowser,
20
+ state,
21
21
  } from "./runtime.js";
22
22
  import type { EffectiveBrowserSettings } from "./settings.js";
23
23
 
24
24
  // Endpoint attachment, managed launch, dynamic-port publication, and shutdown intentionally remain
25
25
  // together because they form one ownership state machine behind the testable operations boundary.
26
26
  interface BrowserSpawnOptions {
27
- shell: false;
28
- stdio: "ignore";
27
+ shell: false;
28
+ stdio: "ignore";
29
29
  }
30
30
 
31
31
  export interface BrowserManagerOperations {
32
- access(path: string, mode: number): Promise<void>;
33
- mkdtemp(prefix: string): Promise<string>;
34
- readFile(path: string, encoding: "utf8"): Promise<string>;
35
- rm(path: string, options: { recursive: true; force: true }): Promise<void>;
36
- fetch(input: string, init?: RequestInit): Promise<Response>;
37
- spawn(executable: string, args: string[], options: BrowserSpawnOptions): ChildProcess;
38
- inspectBrowserVersion(executable: string, signal: AbortSignal): Promise<string>;
39
- isPortAvailable(host: string, port: number): Promise<boolean>;
40
- sleep(ms: number, signal?: AbortSignal): Promise<void>;
32
+ access(path: string, mode: number): Promise<void>;
33
+ mkdtemp(prefix: string): Promise<string>;
34
+ readFile(path: string, encoding: "utf8"): Promise<string>;
35
+ rm(path: string, options: { recursive: true; force: true }): Promise<void>;
36
+ fetch(input: string, init?: RequestInit): Promise<Response>;
37
+ spawn(executable: string, args: string[], options: BrowserSpawnOptions): ChildProcess;
38
+ inspectBrowserVersion(executable: string, signal: AbortSignal): Promise<string>;
39
+ isPortAvailable(host: string, port: number): Promise<boolean>;
40
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
41
41
  }
42
42
 
43
43
  const DEFAULT_BROWSER_MANAGER_OPERATIONS: BrowserManagerOperations = {
44
- access,
45
- mkdtemp,
46
- readFile,
47
- rm: (path, options) => rm(path, options),
48
- fetch: (input, init) => fetch(input, init),
49
- spawn,
50
- inspectBrowserVersion,
51
- isPortAvailable,
52
- sleep: abortableSleep,
44
+ access,
45
+ mkdtemp,
46
+ readFile,
47
+ rm: (path, options) => rm(path, options),
48
+ fetch: (input, init) => fetch(input, init),
49
+ spawn,
50
+ inspectBrowserVersion,
51
+ isPortAvailable,
52
+ sleep: abortableSleep,
53
53
  };
54
54
 
55
55
  let browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
56
56
 
57
57
  interface ManagedBrowserScope {
58
- browser: EffectiveBrowserSettings;
59
- controller: AbortController;
60
- generation: number;
61
- launchPromise?: Promise<void>;
62
- managedBrowser?: ManagedBrowser;
63
- port: number;
64
- lastLaunchAttempt?: BrowserLaunchAttempt;
58
+ browser: EffectiveBrowserSettings;
59
+ controller: AbortController;
60
+ generation: number;
61
+ launchPromise?: Promise<void>;
62
+ managedBrowser?: ManagedBrowser;
63
+ port: number;
64
+ lastLaunchAttempt?: BrowserLaunchAttempt;
65
65
  }
66
66
 
67
67
  const managedBrowserScopes = new WeakMap<object, ManagedBrowserScope>();
68
68
 
69
69
  function cloneBrowserSettings(browser: EffectiveBrowserSettings): EffectiveBrowserSettings {
70
- return { ...browser, extensionPaths: [...browser.extensionPaths] };
70
+ return { ...browser, extensionPaths: [...browser.extensionPaths] };
71
71
  }
72
72
 
73
73
  function runtimeBrowserSettings(): EffectiveBrowserSettings {
74
- return {
75
- endpoint: `http://${formatHostForUrl(state.host)}:${state.configuredPort}`,
76
- host: state.host,
77
- port: state.configuredPort,
78
- hostConfigured: state.hostConfigured,
79
- portConfigured: state.portConfigured,
80
- autoLaunchEnabled: state.autoLaunchEnabled,
81
- ...(state.browserExecutable ? { executablePath: state.browserExecutable } : {}),
82
- extensionPaths: [...state.extensionPaths],
83
- endpointSource: state.endpointSource,
84
- autoLaunchSource: state.autoLaunchSource,
85
- executablePathSource: state.browserExecutableSource,
86
- extensionPathsSource: state.extensionPathsSource,
87
- };
74
+ return {
75
+ endpoint: `http://${formatHostForUrl(state.host)}:${state.configuredPort}`,
76
+ host: state.host,
77
+ port: state.configuredPort,
78
+ hostConfigured: state.hostConfigured,
79
+ portConfigured: state.portConfigured,
80
+ autoLaunchEnabled: state.autoLaunchEnabled,
81
+ ...(state.browserExecutable ? { executablePath: state.browserExecutable } : {}),
82
+ extensionPaths: [...state.extensionPaths],
83
+ endpointSource: state.endpointSource,
84
+ autoLaunchSource: state.autoLaunchSource,
85
+ executablePathSource: state.browserExecutableSource,
86
+ extensionPathsSource: state.extensionPathsSource,
87
+ };
88
88
  }
89
89
 
90
90
  function managedBrowserScope(owner: object) {
91
- const existing = managedBrowserScopes.get(owner);
92
- if (existing) return existing;
93
- const browser = runtimeBrowserSettings();
94
- const created: ManagedBrowserScope = {
95
- browser,
96
- controller: new AbortController(),
97
- generation: 0,
98
- port: browser.port,
99
- };
100
- managedBrowserScopes.set(owner, created);
101
- return created;
91
+ const existing = managedBrowserScopes.get(owner);
92
+ if (existing) return existing;
93
+ const browser = runtimeBrowserSettings();
94
+ const created: ManagedBrowserScope = {
95
+ browser,
96
+ controller: new AbortController(),
97
+ generation: 0,
98
+ port: browser.port,
99
+ };
100
+ managedBrowserScopes.set(owner, created);
101
+ return created;
102
102
  }
103
103
 
104
104
  export function startManagedBrowserSession(owner: object) {
105
- const scope = managedBrowserScope(owner);
106
- scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
107
- scope.controller = new AbortController();
108
- scope.generation += 1;
109
- scope.lastLaunchAttempt = undefined;
105
+ const scope = managedBrowserScope(owner);
106
+ scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
107
+ scope.controller = new AbortController();
108
+ scope.generation += 1;
109
+ scope.lastLaunchAttempt = undefined;
110
110
  }
111
111
 
112
112
  export function syncManagedBrowserSettings(owner: object, browser: EffectiveBrowserSettings) {
113
- const scope = managedBrowserScope(owner);
114
- scope.browser = cloneBrowserSettings(browser);
115
- scope.port = browser.port;
113
+ const scope = managedBrowserScope(owner);
114
+ scope.browser = cloneBrowserSettings(browser);
115
+ scope.port = browser.port;
116
116
  }
117
117
 
118
118
  export function browserSettingsForOwner(owner?: object) {
119
- return owner ? managedBrowserScope(owner).browser : runtimeBrowserSettings();
119
+ return owner ? managedBrowserScope(owner).browser : runtimeBrowserSettings();
120
120
  }
121
121
 
122
122
  export function managedBrowserExtensionPaths(owner?: object) {
123
- return browserSettingsForOwner(owner).extensionPaths;
123
+ return browserSettingsForOwner(owner).extensionPaths;
124
124
  }
125
125
 
126
126
  export function managedBrowserExtensionPathsSource(owner?: object) {
127
- return browserSettingsForOwner(owner).extensionPathsSource;
127
+ return browserSettingsForOwner(owner).extensionPathsSource;
128
128
  }
129
129
 
130
130
  export function managedBrowserForOwner(owner?: object) {
131
- if (!owner) return state.managedBrowser;
132
- const scopedBrowser = managedBrowserScope(owner).managedBrowser;
133
- if (scopedBrowser) return scopedBrowser;
134
- return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : undefined;
131
+ if (!owner) return state.managedBrowser;
132
+ const scopedBrowser = managedBrowserScope(owner).managedBrowser;
133
+ if (scopedBrowser) return scopedBrowser;
134
+ return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : undefined;
135
135
  }
136
136
 
137
137
  function setManagedBrowser(owner: object | undefined, browser: ManagedBrowser | undefined) {
138
- if (owner) managedBrowserScope(owner).managedBrowser = browser;
139
- else state.managedBrowser = browser;
138
+ if (owner) managedBrowserScope(owner).managedBrowser = browser;
139
+ else state.managedBrowser = browser;
140
140
  }
141
141
 
142
142
  function launchPromiseForOwner(owner?: object) {
143
- return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
143
+ return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
144
144
  }
145
145
 
146
146
  function setLaunchPromise(owner: object | undefined, launchPromise: Promise<void> | undefined) {
147
- if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
148
- else state.launchPromise = launchPromise;
147
+ if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
148
+ else state.launchPromise = launchPromise;
149
149
  }
150
150
 
151
151
  function managedBrowserPort(owner?: object) {
152
- return owner ? managedBrowserScope(owner).port : state.port;
152
+ return owner ? managedBrowserScope(owner).port : state.port;
153
153
  }
154
154
 
155
155
  function managedBrowserConfiguredPort(owner?: object) {
156
- return browserSettingsForOwner(owner).port;
156
+ return browserSettingsForOwner(owner).port;
157
157
  }
158
158
 
159
159
  function managedBrowserPortConfigured(owner?: object) {
160
- return browserSettingsForOwner(owner).portConfigured;
160
+ return browserSettingsForOwner(owner).portConfigured;
161
161
  }
162
162
 
163
163
  function setManagedBrowserPort(owner: object | undefined, port: number) {
164
- if (owner) managedBrowserScope(owner).port = port;
165
- else state.port = port;
164
+ if (owner) managedBrowserScope(owner).port = port;
165
+ else state.port = port;
166
166
  }
167
167
 
168
168
  function lastLaunchAttempt(owner?: object) {
169
- return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
169
+ return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
170
170
  }
171
171
 
172
- function setLastLaunchAttempt(
173
- owner: object | undefined,
174
- attempt: BrowserLaunchAttempt | undefined,
175
- ) {
176
- if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
177
- else state.lastLaunchAttempt = attempt;
172
+ function setLastLaunchAttempt(owner: object | undefined, attempt: BrowserLaunchAttempt | undefined) {
173
+ if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
174
+ else state.lastLaunchAttempt = attempt;
178
175
  }
179
176
 
180
177
  export function setBrowserManagerOperationsForTests(overrides: Partial<BrowserManagerOperations>) {
181
- const previous = browserManagerOperations;
182
- browserManagerOperations = { ...DEFAULT_BROWSER_MANAGER_OPERATIONS, ...overrides };
183
- return () => {
184
- browserManagerOperations = previous;
185
- };
178
+ const previous = browserManagerOperations;
179
+ browserManagerOperations = { ...DEFAULT_BROWSER_MANAGER_OPERATIONS, ...overrides };
180
+ return () => {
181
+ browserManagerOperations = previous;
182
+ };
186
183
  }
187
184
 
188
185
  function formatError(error: unknown) {
189
- return error instanceof Error ? error.message : String(error);
186
+ return error instanceof Error ? error.message : String(error);
190
187
  }
191
188
 
192
189
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
193
- return error instanceof Error && "code" in error;
190
+ return error instanceof Error && "code" in error;
194
191
  }
195
192
 
196
193
  function normalizePathForComparison(value: string) {
197
- return process.platform === "win32" ? value.toLowerCase() : value;
194
+ return process.platform === "win32" ? value.toLowerCase() : value;
198
195
  }
199
196
 
200
197
  export async function ensureDevToolsEndpoint(
201
- waitMs = DEFAULT_ENDPOINT_WAIT_MS,
202
- callerSignal?: AbortSignal,
203
- owner?: object,
198
+ waitMs = DEFAULT_ENDPOINT_WAIT_MS,
199
+ callerSignal?: AbortSignal,
200
+ owner?: object,
204
201
  ) {
205
- callerSignal?.throwIfAborted();
206
- if (extensionsConfigured(owner)) {
207
- validateExtensionLaunchMode(owner);
208
- await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
209
- return;
210
- }
211
- if (canAutoLaunchBrowser(owner)) {
212
- try {
213
- await withEndpointRetry(
214
- () => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
215
- waitMs,
216
- callerSignal,
217
- );
218
- return;
219
- } catch (error) {
220
- if (shouldAutoLaunchAfterEndpointError(error, owner)) {
221
- await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
222
- return;
223
- }
224
- throw error;
225
- }
226
- }
227
-
228
- try {
229
- await fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner);
230
- } catch (error) {
231
- if (isRetryableEndpointError(error)) return;
232
- throw error;
233
- }
202
+ callerSignal?.throwIfAborted();
203
+ if (extensionsConfigured(owner)) {
204
+ validateExtensionLaunchMode(owner);
205
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
206
+ return;
207
+ }
208
+ if (canAutoLaunchBrowser(owner)) {
209
+ try {
210
+ await withEndpointRetry(
211
+ () => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
212
+ waitMs,
213
+ callerSignal,
214
+ );
215
+ return;
216
+ } catch (error) {
217
+ if (shouldAutoLaunchAfterEndpointError(error, owner)) {
218
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
219
+ return;
220
+ }
221
+ throw error;
222
+ }
223
+ }
224
+
225
+ try {
226
+ await fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner);
227
+ } catch (error) {
228
+ if (isRetryableEndpointError(error)) return;
229
+ throw error;
230
+ }
234
231
  }
235
232
 
236
233
  function validateExtensionLaunchMode(owner?: object) {
237
- const browser = browserSettingsForOwner(owner);
238
- if (!isLocalDevToolsHost(browser.host)) {
239
- throw new DevToolsEndpointError(
240
- "Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified.",
241
- );
242
- }
243
- if (!browser.autoLaunchEnabled) {
244
- throw new DevToolsEndpointError(
245
- "Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser.",
246
- );
247
- }
248
- if (!browser.executablePath) {
249
- throw new DevToolsEndpointError(
250
- "Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json.",
251
- );
252
- }
253
- }
254
-
255
- async function ensureManagedBrowserLaunched(
256
- waitMs: number,
257
- callerSignal?: AbortSignal,
258
- owner?: object,
259
- ) {
260
- const existingLaunch = launchPromiseForOwner(owner);
261
- if (existingLaunch) {
262
- await waitForCaller(existingLaunch, callerSignal);
263
- return;
264
- }
265
- const existingBrowser = managedBrowserForOwner(owner);
266
- if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
267
- const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
268
- const signal = owner
269
- ? managedBrowserScope(owner).controller.signal
270
- : state.sessionController.signal;
271
- throwIfBrowserLaunchCancelled(generation, signal, owner);
272
-
273
- const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
274
- const wrappedPromise = launchPromise.finally(() => {
275
- if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, undefined);
276
- });
277
- setLaunchPromise(owner, wrappedPromise);
278
- await waitForCaller(wrappedPromise, callerSignal);
234
+ const browser = browserSettingsForOwner(owner);
235
+ if (!isLocalDevToolsHost(browser.host)) {
236
+ throw new DevToolsEndpointError(
237
+ "Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified.",
238
+ );
239
+ }
240
+ if (!browser.autoLaunchEnabled) {
241
+ throw new DevToolsEndpointError(
242
+ "Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser.",
243
+ );
244
+ }
245
+ if (!browser.executablePath) {
246
+ throw new DevToolsEndpointError(
247
+ "Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json.",
248
+ );
249
+ }
250
+ }
251
+
252
+ async function ensureManagedBrowserLaunched(waitMs: number, callerSignal?: AbortSignal, owner?: object) {
253
+ const existingLaunch = launchPromiseForOwner(owner);
254
+ if (existingLaunch) {
255
+ await waitForCaller(existingLaunch, callerSignal);
256
+ return;
257
+ }
258
+ const existingBrowser = managedBrowserForOwner(owner);
259
+ if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
260
+ const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
261
+ const signal = owner ? managedBrowserScope(owner).controller.signal : state.sessionController.signal;
262
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
263
+
264
+ const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
265
+ const wrappedPromise = launchPromise.finally(() => {
266
+ if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, undefined);
267
+ });
268
+ setLaunchPromise(owner, wrappedPromise);
269
+ await waitForCaller(wrappedPromise, callerSignal);
279
270
  }
280
271
 
281
272
  function waitForCaller<T>(operation: Promise<T>, signal?: AbortSignal) {
282
- if (!signal) return operation;
283
- signal.throwIfAborted();
284
- return new Promise<T>((resolve, reject) => {
285
- let settled = false;
286
- const onAbort = () => settle(() => reject(signal.reason));
287
- const settle = (callback: () => void) => {
288
- if (settled) return;
289
- settled = true;
290
- signal.removeEventListener("abort", onAbort);
291
- callback();
292
- };
293
- signal.addEventListener("abort", onAbort, { once: true });
294
- operation.then(
295
- (value) => settle(() => resolve(value)),
296
- (error) => settle(() => reject(error)),
297
- );
298
- });
299
- }
300
-
301
- async function prepareManagedBrowserLaunch(
302
- waitMs: number,
303
- generation: number,
304
- signal: AbortSignal,
305
- owner?: object,
306
- ) {
307
- const existingBrowser = managedBrowserForOwner(owner);
308
- if (existingBrowser) {
309
- await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
310
- throwIfBrowserLaunchCancelled(generation, signal, owner);
311
- }
312
- if (managedBrowserPortConfigured(owner)) {
313
- const available = await browserManagerOperations.isPortAvailable(
314
- browserSettingsForOwner(owner).host,
315
- managedBrowserPort(owner),
316
- );
317
- throwIfBrowserLaunchCancelled(generation, signal, owner);
318
- if (!available) {
319
- throw new DevToolsEndpointError(
320
- `Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
321
- );
322
- }
323
- }
324
- return launchManagedBrowser(waitMs, generation, signal, owner);
325
- }
326
-
327
- async function launchManagedBrowser(
328
- waitMs: number,
329
- generation: number,
330
- signal: AbortSignal,
331
- owner?: object,
332
- ) {
333
- throwIfBrowserLaunchCancelled(generation, signal, owner);
334
- const candidateDefinitions = browserCandidateDefinitions(owner);
335
- const candidates = await resolveBrowserCandidates(
336
- candidateDefinitions,
337
- generation,
338
- signal,
339
- owner,
340
- );
341
- throwIfBrowserLaunchCancelled(generation, signal, owner);
342
- setLastLaunchAttempt(owner, {
343
- candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
344
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
345
- });
346
-
347
- if (candidates.length === 0) {
348
- throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
349
- }
350
-
351
- let lastError: unknown;
352
- for (const candidate of candidates) {
353
- throwIfBrowserLaunchCancelled(generation, signal, owner);
354
- try {
355
- if (extensionsConfigured(owner))
356
- await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
357
- await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
358
- throwIfBrowserLaunchCancelled(generation, signal, owner);
359
- setLastLaunchAttempt(owner, {
360
- ...lastLaunchAttempt(owner),
361
- candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
362
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
363
- selectedCandidate: formatBrowserCandidate(candidate),
364
- userDataDir: managedBrowserForOwner(owner)?.userDataDir,
365
- });
366
- return;
367
- } catch (error) {
368
- try {
369
- throwIfBrowserLaunchCancelled(generation, signal, owner);
370
- } catch (cancellation) {
371
- setLastLaunchAttempt(owner, {
372
- ...(lastLaunchAttempt(owner) ?? {
373
- candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
374
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
375
- }),
376
- lastError: formatError(cancellation),
377
- });
378
- throw cancellation;
379
- }
380
- lastError = error;
381
- setLastLaunchAttempt(owner, {
382
- ...(lastLaunchAttempt(owner) ?? {
383
- candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
384
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
385
- }),
386
- lastError: formatError(error),
387
- });
388
- }
389
- }
390
-
391
- if (lastError instanceof DevToolsEndpointError && extensionsConfigured(owner)) throw lastError;
392
- throw new DevToolsEndpointError(
393
- [
394
- "Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
395
- `Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
396
- lastError ? `Last error: ${formatError(lastError)}` : undefined,
397
- launchHint(owner),
398
- endpointConfigHint(),
399
- ]
400
- .filter(Boolean)
401
- .join("\n"),
402
- );
273
+ if (!signal) return operation;
274
+ signal.throwIfAborted();
275
+ return new Promise<T>((resolve, reject) => {
276
+ let settled = false;
277
+ const onAbort = () => settle(() => reject(signal.reason));
278
+ const settle = (callback: () => void) => {
279
+ if (settled) return;
280
+ settled = true;
281
+ signal.removeEventListener("abort", onAbort);
282
+ callback();
283
+ };
284
+ signal.addEventListener("abort", onAbort, { once: true });
285
+ operation.then(
286
+ (value) => settle(() => resolve(value)),
287
+ (error) => settle(() => reject(error)),
288
+ );
289
+ });
290
+ }
291
+
292
+ async function prepareManagedBrowserLaunch(waitMs: number, generation: number, signal: AbortSignal, owner?: object) {
293
+ const existingBrowser = managedBrowserForOwner(owner);
294
+ if (existingBrowser) {
295
+ await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
296
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
297
+ }
298
+ if (managedBrowserPortConfigured(owner)) {
299
+ const available = await browserManagerOperations.isPortAvailable(
300
+ browserSettingsForOwner(owner).host,
301
+ managedBrowserPort(owner),
302
+ );
303
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
304
+ if (!available) {
305
+ throw new DevToolsEndpointError(
306
+ `Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
307
+ );
308
+ }
309
+ }
310
+ return launchManagedBrowser(waitMs, generation, signal, owner);
311
+ }
312
+
313
+ async function launchManagedBrowser(waitMs: number, generation: number, signal: AbortSignal, owner?: object) {
314
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
315
+ const candidateDefinitions = browserCandidateDefinitions(owner);
316
+ const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal, owner);
317
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
318
+ setLastLaunchAttempt(owner, {
319
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
320
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
321
+ });
322
+
323
+ if (candidates.length === 0) {
324
+ throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
325
+ }
326
+
327
+ let lastError: unknown;
328
+ for (const candidate of candidates) {
329
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
330
+ try {
331
+ if (extensionsConfigured(owner)) await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
332
+ await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
333
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
334
+ setLastLaunchAttempt(owner, {
335
+ ...lastLaunchAttempt(owner),
336
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
337
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
338
+ selectedCandidate: formatBrowserCandidate(candidate),
339
+ userDataDir: managedBrowserForOwner(owner)?.userDataDir,
340
+ });
341
+ return;
342
+ } catch (error) {
343
+ try {
344
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
345
+ } catch (cancellation) {
346
+ setLastLaunchAttempt(owner, {
347
+ ...(lastLaunchAttempt(owner) ?? {
348
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
349
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
350
+ }),
351
+ lastError: formatError(cancellation),
352
+ });
353
+ throw cancellation;
354
+ }
355
+ lastError = error;
356
+ setLastLaunchAttempt(owner, {
357
+ ...(lastLaunchAttempt(owner) ?? {
358
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
359
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
360
+ }),
361
+ lastError: formatError(error),
362
+ });
363
+ }
364
+ }
365
+
366
+ if (lastError instanceof DevToolsEndpointError && extensionsConfigured(owner)) throw lastError;
367
+ throw new DevToolsEndpointError(
368
+ [
369
+ "Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
370
+ `Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
371
+ lastError ? `Last error: ${formatError(lastError)}` : undefined,
372
+ launchHint(owner),
373
+ endpointConfigHint(),
374
+ ]
375
+ .filter(Boolean)
376
+ .join("\n"),
377
+ );
403
378
  }
404
379
 
405
380
  async function requireSupportedExtensionBrowser(
406
- candidate: BrowserCandidate,
407
- generation: number,
408
- signal: AbortSignal,
409
- owner?: object,
381
+ candidate: BrowserCandidate,
382
+ generation: number,
383
+ signal: AbortSignal,
384
+ owner?: object,
410
385
  ) {
411
- let version: string;
412
- try {
413
- version = (
414
- await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)
415
- ).trim();
416
- } catch (error) {
417
- throwIfBrowserLaunchCancelled(generation, signal, owner);
418
- throw new DevToolsEndpointError(
419
- `Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError(error)}. Configure Chrome for Testing or Chromium.`,
420
- );
421
- }
422
- throwIfBrowserLaunchCancelled(generation, signal, owner);
423
- const classification = classifyExtensionBrowserVersion(version);
424
- if (!classification.supported) {
425
- throw new DevToolsEndpointError(
426
- `Unsupported browser for unpacked extensions: ${version || "unknown product"}. Configure Chrome for Testing or Chromium; branded Chrome may silently ignore extension startup flags.`,
427
- );
428
- }
386
+ let version: string;
387
+ try {
388
+ version = (await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)).trim();
389
+ } catch (error) {
390
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
391
+ throw new DevToolsEndpointError(
392
+ `Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError(error)}. Configure Chrome for Testing or Chromium.`,
393
+ );
394
+ }
395
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
396
+ const classification = classifyExtensionBrowserVersion(version);
397
+ if (!classification.supported) {
398
+ throw new DevToolsEndpointError(
399
+ `Unsupported browser for unpacked extensions: ${version || "unknown product"}. Configure Chrome for Testing or Chromium; branded Chrome may silently ignore extension startup flags.`,
400
+ );
401
+ }
429
402
  }
430
403
 
431
404
  export function classifyExtensionBrowserVersion(version: string): {
432
- supported: boolean;
433
- product: string;
405
+ supported: boolean;
406
+ product: string;
434
407
  } {
435
- if (/^(?:Google )?Chrome for Testing\b/i.test(version)) {
436
- return { supported: true, product: "Chrome for Testing" };
437
- }
438
- if (/^Chromium\b/i.test(version)) return { supported: true, product: "Chromium" };
439
- const product = version.trim().replace(/\s+\d+(?:\.\d+)*.*$/, "") || "Unknown";
440
- return { supported: false, product };
408
+ if (/^(?:Google )?Chrome for Testing\b/i.test(version)) {
409
+ return { supported: true, product: "Chrome for Testing" };
410
+ }
411
+ if (/^Chromium\b/i.test(version)) return { supported: true, product: "Chromium" };
412
+ const product = version.trim().replace(/\s+\d+(?:\.\d+)*.*$/, "") || "Unknown";
413
+ return { supported: false, product };
441
414
  }
442
415
 
443
416
  async function launchBrowserCandidate(
444
- candidate: BrowserCandidate,
445
- waitMs: number,
446
- generation: number,
447
- signal: AbortSignal,
448
- owner?: object,
417
+ candidate: BrowserCandidate,
418
+ waitMs: number,
419
+ generation: number,
420
+ signal: AbortSignal,
421
+ owner?: object,
449
422
  ) {
450
- throwIfBrowserLaunchCancelled(generation, signal, owner);
451
- const userDataDir = await browserManagerOperations.mkdtemp(
452
- join(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX),
453
- );
454
- let managedBrowser: ManagedBrowser | undefined;
455
- try {
456
- throwIfBrowserLaunchCancelled(generation, signal, owner);
457
- const portArgument = managedBrowserPortConfigured(owner)
458
- ? String(managedBrowserPort(owner))
459
- : "0";
460
- const args = buildManagedBrowserLaunchArguments(
461
- userDataDir,
462
- portArgument,
463
- managedBrowserExtensionPaths(owner),
464
- );
465
- const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
466
- shell: false,
467
- stdio: "ignore",
468
- });
469
- const launchedBrowser: ManagedBrowser = {
470
- process: child,
471
- userDataDir,
472
- exited: false,
473
- ready: false,
474
- ownerGeneration: generation,
475
- ...(owner ? { sessionOwner: owner } : {}),
476
- };
477
- managedBrowser = launchedBrowser;
478
- setManagedBrowser(owner, launchedBrowser);
479
-
480
- child.once("exit", () => {
481
- if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
482
- invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
483
- }
484
- launchedBrowser.exited = true;
485
- launchedBrowser.ready = false;
486
- if (
487
- !managedBrowserPortConfigured(owner) &&
488
- launchedBrowser.port === managedBrowserPort(owner)
489
- ) {
490
- setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
491
- }
492
- });
493
-
494
- await waitForBrowserSpawn(child);
495
- throwIfBrowserLaunchCancelled(generation, signal, owner);
496
- if (managedBrowserPortConfigured(owner)) {
497
- launchedBrowser.port = managedBrowserPort(owner);
498
- } else {
499
- launchedBrowser.port = await readManagedBrowserPort(
500
- userDataDir,
501
- launchedBrowser,
502
- waitMs,
503
- generation,
504
- signal,
505
- owner,
506
- );
507
- throwIfBrowserLaunchCancelled(generation, signal, owner);
508
- setManagedBrowserPort(owner, launchedBrowser.port);
509
- }
510
- await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
511
- throwIfBrowserLaunchCancelled(generation, signal, owner);
512
- launchedBrowser.ready = true;
513
- } catch (error) {
514
- if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
515
- else {
516
- await browserManagerOperations
517
- .rm(userDataDir, { recursive: true, force: true })
518
- .catch(() => undefined);
519
- }
520
- throw error;
521
- }
423
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
424
+ const userDataDir = await browserManagerOperations.mkdtemp(join(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX));
425
+ let managedBrowser: ManagedBrowser | undefined;
426
+ try {
427
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
428
+ const portArgument = managedBrowserPortConfigured(owner) ? String(managedBrowserPort(owner)) : "0";
429
+ const args = buildManagedBrowserLaunchArguments(userDataDir, portArgument, managedBrowserExtensionPaths(owner));
430
+ const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
431
+ shell: false,
432
+ stdio: "ignore",
433
+ });
434
+ const launchedBrowser: ManagedBrowser = {
435
+ process: child,
436
+ userDataDir,
437
+ exited: false,
438
+ ready: false,
439
+ ownerGeneration: generation,
440
+ ...(owner ? { sessionOwner: owner } : {}),
441
+ };
442
+ managedBrowser = launchedBrowser;
443
+ setManagedBrowser(owner, launchedBrowser);
444
+
445
+ child.once("exit", () => {
446
+ if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
447
+ invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
448
+ }
449
+ launchedBrowser.exited = true;
450
+ launchedBrowser.ready = false;
451
+ if (!managedBrowserPortConfigured(owner) && launchedBrowser.port === managedBrowserPort(owner)) {
452
+ setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
453
+ }
454
+ });
455
+
456
+ await waitForBrowserSpawn(child);
457
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
458
+ if (managedBrowserPortConfigured(owner)) {
459
+ launchedBrowser.port = managedBrowserPort(owner);
460
+ } else {
461
+ launchedBrowser.port = await readManagedBrowserPort(
462
+ userDataDir,
463
+ launchedBrowser,
464
+ waitMs,
465
+ generation,
466
+ signal,
467
+ owner,
468
+ );
469
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
470
+ setManagedBrowserPort(owner, launchedBrowser.port);
471
+ }
472
+ await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
473
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
474
+ launchedBrowser.ready = true;
475
+ } catch (error) {
476
+ if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
477
+ else {
478
+ await browserManagerOperations.rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
479
+ }
480
+ throw error;
481
+ }
522
482
  }
523
483
 
524
484
  export function buildManagedBrowserLaunchArguments(
525
- userDataDir: string,
526
- portArgument: string,
527
- extensionPaths: readonly string[],
485
+ userDataDir: string,
486
+ portArgument: string,
487
+ extensionPaths: readonly string[],
528
488
  ) {
529
- const extensionList = extensionPaths.join(",");
530
- return [
531
- `--remote-debugging-port=${portArgument}`,
532
- `--user-data-dir=${userDataDir}`,
533
- ...(extensionPaths.length > 0
534
- ? [`--disable-extensions-except=${extensionList}`, `--load-extension=${extensionList}`]
535
- : []),
536
- // Chrome 138+ de-elevates when launched from an elevated process on Windows: the
537
- // spawned process relaunches a de-elevated child and exits immediately, which the
538
- // exit watchdog misreads as a failed launch.
539
- ...(process.platform === "win32" ? ["--do-not-de-elevate"] : []),
540
- "--no-first-run",
541
- "--no-default-browser-check",
542
- "about:blank",
543
- ];
489
+ const extensionList = extensionPaths.join(",");
490
+ return [
491
+ `--remote-debugging-port=${portArgument}`,
492
+ `--user-data-dir=${userDataDir}`,
493
+ ...(extensionPaths.length > 0
494
+ ? [`--disable-extensions-except=${extensionList}`, `--load-extension=${extensionList}`]
495
+ : []),
496
+ // Chrome 138+ de-elevates when launched from an elevated process on Windows: the
497
+ // spawned process relaunches a de-elevated child and exits immediately, which the
498
+ // exit watchdog misreads as a failed launch.
499
+ ...(process.platform === "win32" ? ["--do-not-de-elevate"] : []),
500
+ "--no-first-run",
501
+ "--no-default-browser-check",
502
+ "about:blank",
503
+ ];
544
504
  }
545
505
 
546
506
  function waitForBrowserSpawn(child: ChildProcess) {
547
- return new Promise<void>((resolve, reject) => {
548
- let settled = false;
549
- const settle = (callback: () => void) => {
550
- if (settled) return;
551
- settled = true;
552
- child.off("error", onError);
553
- child.off("spawn", onSpawn);
554
- callback();
555
- };
556
- const onError = (error: Error) => settle(() => reject(error));
557
- const onSpawn = () => settle(resolve);
558
- child.once("error", onError);
559
- child.once("spawn", onSpawn);
560
- });
507
+ return new Promise<void>((resolve, reject) => {
508
+ let settled = false;
509
+ const settle = (callback: () => void) => {
510
+ if (settled) return;
511
+ settled = true;
512
+ child.off("error", onError);
513
+ child.off("spawn", onSpawn);
514
+ callback();
515
+ };
516
+ const onError = (error: Error) => settle(() => reject(error));
517
+ const onSpawn = () => settle(resolve);
518
+ child.once("error", onError);
519
+ child.once("spawn", onSpawn);
520
+ });
561
521
  }
562
522
 
563
523
  async function readManagedBrowserPort(
564
- userDataDir: string,
565
- managedBrowser: ManagedBrowser,
566
- waitMs: number,
567
- generation: number,
568
- signal: AbortSignal,
569
- owner?: object,
524
+ userDataDir: string,
525
+ managedBrowser: ManagedBrowser,
526
+ waitMs: number,
527
+ generation: number,
528
+ signal: AbortSignal,
529
+ owner?: object,
570
530
  ) {
571
- const activePortFile = join(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
572
- const deadline = Date.now() + waitMs;
573
- while (true) {
574
- throwIfManagedBrowserExited(managedBrowser);
575
- const text = await browserManagerOperations
576
- .readFile(activePortFile, "utf8")
577
- .catch((error: unknown) => {
578
- if (isNodeError(error) && error.code === "ENOENT") return undefined;
579
- throw error;
580
- });
581
- throwIfBrowserLaunchCancelled(generation, signal, owner);
582
- const portText = text?.split(/\r?\n/, 1)[0]?.trim();
583
- const port = Number(portText);
584
- if (Number.isInteger(port) && port > 0) return port;
585
-
586
- const remainingMs = deadline - Date.now();
587
- if (remainingMs <= 0) {
588
- throw new DevToolsEndpointError(
589
- [
590
- "Timed out waiting for auto-launched browser DevToolsActivePort.",
591
- `Expected file: ${activePortFile}`,
592
- launchHint(owner),
593
- ].join("\n"),
594
- );
595
- }
596
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
597
- }
531
+ const activePortFile = join(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
532
+ const deadline = Date.now() + waitMs;
533
+ while (true) {
534
+ throwIfManagedBrowserExited(managedBrowser);
535
+ const text = await browserManagerOperations.readFile(activePortFile, "utf8").catch((error: unknown) => {
536
+ if (isNodeError(error) && error.code === "ENOENT") return undefined;
537
+ throw error;
538
+ });
539
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
540
+ const portText = text?.split(/\r?\n/, 1)[0]?.trim();
541
+ const port = Number(portText);
542
+ if (Number.isInteger(port) && port > 0) return port;
543
+
544
+ const remainingMs = deadline - Date.now();
545
+ if (remainingMs <= 0) {
546
+ throw new DevToolsEndpointError(
547
+ [
548
+ "Timed out waiting for auto-launched browser DevToolsActivePort.",
549
+ `Expected file: ${activePortFile}`,
550
+ launchHint(owner),
551
+ ].join("\n"),
552
+ );
553
+ }
554
+ await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
555
+ }
598
556
  }
599
557
 
600
558
  async function waitForDevToolsEndpoint(
601
- waitMs: number,
602
- managedBrowser: ManagedBrowser,
603
- generation: number,
604
- signal: AbortSignal,
605
- owner?: object,
559
+ waitMs: number,
560
+ managedBrowser: ManagedBrowser,
561
+ generation: number,
562
+ signal: AbortSignal,
563
+ owner?: object,
606
564
  ) {
607
- const deadline = Date.now() + waitMs;
608
- while (true) {
609
- throwIfManagedBrowserExited(managedBrowser);
610
- try {
611
- await fetchDevToolsJson<unknown>("/json/version", { signal }, owner);
612
- throwIfBrowserLaunchCancelled(generation, signal, owner);
613
- return;
614
- } catch (error) {
615
- if (!isRetryableEndpointError(error)) throw error;
616
- }
617
-
618
- throwIfBrowserLaunchCancelled(generation, signal, owner);
619
- const remainingMs = deadline - Date.now();
620
- if (remainingMs <= 0) {
621
- throw new DevToolsEndpointError(
622
- [
623
- `Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
624
- launchHint(owner),
625
- ].join("\n"),
626
- );
627
- }
628
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
629
- }
565
+ const deadline = Date.now() + waitMs;
566
+ while (true) {
567
+ throwIfManagedBrowserExited(managedBrowser);
568
+ try {
569
+ await fetchDevToolsJson<unknown>("/json/version", { signal }, owner);
570
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
571
+ return;
572
+ } catch (error) {
573
+ if (!isRetryableEndpointError(error)) throw error;
574
+ }
575
+
576
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
577
+ const remainingMs = deadline - Date.now();
578
+ if (remainingMs <= 0) {
579
+ throw new DevToolsEndpointError(
580
+ [`Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`, launchHint(owner)].join("\n"),
581
+ );
582
+ }
583
+ await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
584
+ }
630
585
  }
631
586
 
632
587
  function throwIfManagedBrowserExited(managedBrowser: ManagedBrowser) {
633
- if (!managedBrowser.exited) return;
634
- throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
588
+ if (!managedBrowser.exited) return;
589
+ throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
635
590
  }
636
591
 
637
592
  function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal, owner?: object) {
638
- const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
639
- const shuttingDown = owner ? signal.aborted : state.shuttingDown;
640
- if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
641
- throw new DevToolsEndpointError(
642
- generation === currentGeneration
643
- ? "Chrome DevTools browser launch cancelled during shutdown."
644
- : "Chrome DevTools browser launch cancelled because the session was replaced.",
645
- );
593
+ const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
594
+ const shuttingDown = owner ? signal.aborted : state.shuttingDown;
595
+ if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
596
+ throw new DevToolsEndpointError(
597
+ generation === currentGeneration
598
+ ? "Chrome DevTools browser launch cancelled during shutdown."
599
+ : "Chrome DevTools browser launch cancelled because the session was replaced.",
600
+ );
646
601
  }
647
602
 
648
603
  export async function shutdownManagedBrowser(
649
- managedBrowser?: ManagedBrowser,
650
- options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
604
+ managedBrowser?: ManagedBrowser,
605
+ options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
651
606
  ) {
652
- const owner = options.owner ?? managedBrowser?.sessionOwner;
653
- if (options.cancelLaunch) {
654
- if (owner) {
655
- const scope = managedBrowserScope(owner);
656
- scope.controller.abort(
657
- new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError"),
658
- );
659
- scope.generation += 1;
660
- } else {
661
- state.shuttingDown = true;
662
- }
663
- }
664
- managedBrowser ??= managedBrowserForOwner(owner);
665
- if (options.awaitLaunch !== false) {
666
- await launchPromiseForOwner(owner)?.catch(() => undefined);
667
- managedBrowser ??= managedBrowserForOwner(owner);
668
- }
669
- if (!managedBrowser) return;
670
- if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
671
-
672
- const cleanup = cleanupManagedBrowser(managedBrowser, owner);
673
- managedBrowser.cleanupPromise = cleanup;
674
- return cleanup;
607
+ const owner = options.owner ?? managedBrowser?.sessionOwner;
608
+ if (options.cancelLaunch) {
609
+ if (owner) {
610
+ const scope = managedBrowserScope(owner);
611
+ scope.controller.abort(
612
+ new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError"),
613
+ );
614
+ scope.generation += 1;
615
+ } else {
616
+ state.shuttingDown = true;
617
+ }
618
+ }
619
+ managedBrowser ??= managedBrowserForOwner(owner);
620
+ if (options.awaitLaunch !== false) {
621
+ await launchPromiseForOwner(owner)?.catch(() => undefined);
622
+ managedBrowser ??= managedBrowserForOwner(owner);
623
+ }
624
+ if (!managedBrowser) return;
625
+ if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
626
+
627
+ const cleanup = cleanupManagedBrowser(managedBrowser, owner);
628
+ managedBrowser.cleanupPromise = cleanup;
629
+ return cleanup;
675
630
  }
676
631
 
677
632
  async function cleanupManagedBrowser(managedBrowser: ManagedBrowser, owner?: object) {
678
- if (managedBrowserForOwner(owner) === managedBrowser) {
679
- if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
680
- if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
681
- else setManagedBrowser(owner, undefined);
682
- }
683
- if (!managedBrowser.exited) {
684
- killManagedBrowserProcess(managedBrowser);
685
- await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
686
- killManagedBrowserProcess(managedBrowser, "SIGKILL");
687
- await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(
688
- () => undefined,
689
- );
690
- });
691
- }
692
- await browserManagerOperations
693
- .rm(managedBrowser.userDataDir, { recursive: true, force: true })
694
- .catch(() => undefined);
695
- if (!managedBrowserPortConfigured(owner) && managedBrowser.port === managedBrowserPort(owner)) {
696
- setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
697
- }
633
+ if (managedBrowserForOwner(owner) === managedBrowser) {
634
+ if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
635
+ if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
636
+ else setManagedBrowser(owner, undefined);
637
+ }
638
+ if (!managedBrowser.exited) {
639
+ killManagedBrowserProcess(managedBrowser);
640
+ await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
641
+ killManagedBrowserProcess(managedBrowser, "SIGKILL");
642
+ await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(() => undefined);
643
+ });
644
+ }
645
+ await browserManagerOperations
646
+ .rm(managedBrowser.userDataDir, { recursive: true, force: true })
647
+ .catch(() => undefined);
648
+ if (!managedBrowserPortConfigured(owner) && managedBrowser.port === managedBrowserPort(owner)) {
649
+ setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
650
+ }
698
651
  }
699
652
 
700
653
  function killManagedBrowserProcess(managedBrowser: ManagedBrowser, signal?: NodeJS.Signals) {
701
- try {
702
- managedBrowser.process.kill(signal);
703
- } catch {
704
- // Best-effort shutdown: the browser may have already exited or failed to spawn.
705
- }
654
+ try {
655
+ managedBrowser.process.kill(signal);
656
+ } catch {
657
+ // Best-effort shutdown: the browser may have already exited or failed to spawn.
658
+ }
706
659
  }
707
660
 
708
661
  function waitForManagedBrowserExit(managedBrowser: ManagedBrowser, waitMs: number) {
709
- if (managedBrowser.exited) return Promise.resolve();
710
- return new Promise<void>((resolve, reject) => {
711
- const settle = (callback: () => void) => {
712
- clearTimeout(timeout);
713
- managedBrowser.process.off("exit", onExitOrClose);
714
- managedBrowser.process.off("close", onExitOrClose);
715
- callback();
716
- };
717
- const onExitOrClose = () => {
718
- managedBrowser.exited = true;
719
- settle(resolve);
720
- };
721
- const timeout = setTimeout(
722
- () => settle(() => reject(new Error("Timed out waiting for browser shutdown."))),
723
- waitMs,
724
- );
725
- managedBrowser.process.once("exit", onExitOrClose);
726
- managedBrowser.process.once("close", onExitOrClose);
727
- });
662
+ if (managedBrowser.exited) return Promise.resolve();
663
+ return new Promise<void>((resolve, reject) => {
664
+ const settle = (callback: () => void) => {
665
+ clearTimeout(timeout);
666
+ managedBrowser.process.off("exit", onExitOrClose);
667
+ managedBrowser.process.off("close", onExitOrClose);
668
+ callback();
669
+ };
670
+ const onExitOrClose = () => {
671
+ managedBrowser.exited = true;
672
+ settle(resolve);
673
+ };
674
+ const timeout = setTimeout(
675
+ () => settle(() => reject(new Error("Timed out waiting for browser shutdown."))),
676
+ waitMs,
677
+ );
678
+ managedBrowser.process.once("exit", onExitOrClose);
679
+ managedBrowser.process.once("close", onExitOrClose);
680
+ });
728
681
  }
729
682
 
730
683
  export async function fetchDevToolsJson<T>(path: string, init?: RequestInit, owner?: object) {
731
- const url = `${devToolsEndpoint(owner)}${path}`;
732
- let response: Response;
733
- try {
734
- const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
735
- response = await browserManagerOperations.fetch(url, {
736
- ...init,
737
- signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal,
738
- });
739
- } catch (error) {
740
- throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
741
- launchable: true,
742
- retryable: true,
743
- });
744
- }
745
-
746
- if (!response.ok) {
747
- const body = (await response.text().catch(() => "")).trim();
748
- const suffix = body ? `: ${body.slice(0, 200)}` : "";
749
- throw new DevToolsEndpointError(
750
- [
751
- `Chrome DevTools endpoint ${url} returned ${response.status} ${response.statusText}${suffix}.`,
752
- endpointConfigHint(),
753
- ].join("\n"),
754
- { retryable: response.status === 429 || response.status >= 500 },
755
- );
756
- }
757
-
758
- try {
759
- return (await response.json()) as T;
760
- } catch (error) {
761
- throw new DevToolsEndpointError(
762
- [
763
- `Chrome DevTools endpoint ${url} returned invalid JSON: ${formatError(error)}.`,
764
- endpointConfigHint(),
765
- ].join("\n"),
766
- );
767
- }
768
- }
769
-
770
- export async function withEndpointRetry<T>(
771
- operation: () => Promise<T>,
772
- waitMs: number,
773
- signal?: AbortSignal,
774
- ) {
775
- const deadline = Date.now() + waitMs;
776
- while (true) {
777
- signal?.throwIfAborted();
778
- try {
779
- return await operation();
780
- } catch (error) {
781
- if (!isRetryableEndpointError(error)) throw error;
782
-
783
- const remainingMs = deadline - Date.now();
784
- if (remainingMs <= 0) throw error;
785
-
786
- await browserManagerOperations.sleep(
787
- Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
788
- signal,
789
- );
790
- }
791
- }
684
+ const url = `${devToolsEndpoint(owner)}${path}`;
685
+ let response: Response;
686
+ try {
687
+ const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
688
+ response = await browserManagerOperations.fetch(url, {
689
+ ...init,
690
+ signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal,
691
+ });
692
+ } catch (error) {
693
+ throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
694
+ launchable: true,
695
+ retryable: true,
696
+ });
697
+ }
698
+
699
+ if (!response.ok) {
700
+ const body = (await response.text().catch(() => "")).trim();
701
+ const suffix = body ? `: ${body.slice(0, 200)}` : "";
702
+ throw new DevToolsEndpointError(
703
+ [
704
+ `Chrome DevTools endpoint ${url} returned ${response.status} ${response.statusText}${suffix}.`,
705
+ endpointConfigHint(),
706
+ ].join("\n"),
707
+ { retryable: response.status === 429 || response.status >= 500 },
708
+ );
709
+ }
710
+
711
+ try {
712
+ return (await response.json()) as T;
713
+ } catch (error) {
714
+ throw new DevToolsEndpointError(
715
+ [`Chrome DevTools endpoint ${url} returned invalid JSON: ${formatError(error)}.`, endpointConfigHint()].join(
716
+ "\n",
717
+ ),
718
+ );
719
+ }
720
+ }
721
+
722
+ export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs: number, signal?: AbortSignal) {
723
+ const deadline = Date.now() + waitMs;
724
+ while (true) {
725
+ signal?.throwIfAborted();
726
+ try {
727
+ return await operation();
728
+ } catch (error) {
729
+ if (!isRetryableEndpointError(error)) throw error;
730
+
731
+ const remainingMs = deadline - Date.now();
732
+ if (remainingMs <= 0) throw error;
733
+
734
+ await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
735
+ }
736
+ }
792
737
  }
793
738
 
794
739
  function isRetryableEndpointError(error: unknown) {
795
- return error instanceof DevToolsEndpointError && error.retryable;
740
+ return error instanceof DevToolsEndpointError && error.retryable;
796
741
  }
797
742
 
798
743
  function isLaunchableEndpointError(error: unknown) {
799
- return error instanceof DevToolsEndpointError && error.launchable;
744
+ return error instanceof DevToolsEndpointError && error.launchable;
800
745
  }
801
746
 
802
747
  function shouldAutoLaunchAfterEndpointError(error: unknown, owner?: object) {
803
- if (!canAutoLaunchBrowser(owner)) return false;
804
- if (isLaunchableEndpointError(error)) return true;
748
+ if (!canAutoLaunchBrowser(owner)) return false;
749
+ if (isLaunchableEndpointError(error)) return true;
805
750
 
806
- // After the attach-first attempt (including its retry window, when applicable) fails, treat any
807
- // DevTools endpoint error on an unpinned port as a conflict we can avoid with a dynamic port.
808
- return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
751
+ // After the attach-first attempt (including its retry window, when applicable) fails, treat any
752
+ // DevTools endpoint error on an unpinned port as a conflict we can avoid with a dynamic port.
753
+ return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
809
754
  }
810
755
 
811
756
  function canAutoLaunchBrowser(owner?: object) {
812
- const browser = browserSettingsForOwner(owner);
813
- return browser.autoLaunchEnabled && isLocalDevToolsHost(browser.host);
757
+ const browser = browserSettingsForOwner(owner);
758
+ return browser.autoLaunchEnabled && isLocalDevToolsHost(browser.host);
814
759
  }
815
760
 
816
761
  function extensionsConfigured(owner?: object) {
817
- return managedBrowserExtensionPaths(owner).length > 0;
762
+ return managedBrowserExtensionPaths(owner).length > 0;
818
763
  }
819
764
 
820
765
  function endpointConnectionErrorMessage(error: unknown, owner?: object) {
821
- const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
822
- return [
823
- `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
824
- launchHint(owner),
825
- endpointConfigHint(),
826
- ].join("\n");
766
+ const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
767
+ return [
768
+ `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
769
+ launchHint(owner),
770
+ endpointConfigHint(),
771
+ ].join("\n");
827
772
  }
828
773
 
829
774
  function isTimeoutError(error: unknown) {
830
- return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
775
+ return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
831
776
  }
832
777
 
833
778
  export function devToolsEndpoint(owner?: object) {
834
- return `http://${formatHostForUrl(browserSettingsForOwner(owner).host)}:${managedBrowserPort(owner)}`;
779
+ return `http://${formatHostForUrl(browserSettingsForOwner(owner).host)}:${managedBrowserPort(owner)}`;
835
780
  }
836
781
 
837
782
  export function formatHostForUrl(host: string) {
838
- if (host.startsWith("[") && host.endsWith("]")) return host;
839
- return host.includes(":") ? `[${host}]` : host;
783
+ if (host.startsWith("[") && host.endsWith("]")) return host;
784
+ return host.includes(":") ? `[${host}]` : host;
840
785
  }
841
786
 
842
787
  export function endpointSourceLabel(owner?: object) {
843
- return browserSettingsForOwner(owner).endpointSource;
788
+ return browserSettingsForOwner(owner).endpointSource;
844
789
  }
845
790
 
846
791
  export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
847
792
 
848
793
  export function browserLifecycleState(owner?: object): BrowserLifecycleState {
849
- if (launchPromiseForOwner(owner)) return "starting";
850
- if (managedBrowserForOwner(owner)?.exited) return "exited";
851
- if (managedBrowserForOwner(owner)?.ready) return "running";
852
- if (lastLaunchAttempt(owner)?.lastError) return "failed";
853
- return "unobserved";
794
+ if (launchPromiseForOwner(owner)) return "starting";
795
+ if (managedBrowserForOwner(owner)?.exited) return "exited";
796
+ if (managedBrowserForOwner(owner)?.ready) return "running";
797
+ if (lastLaunchAttempt(owner)?.lastError) return "failed";
798
+ return "unobserved";
854
799
  }
855
800
 
856
801
  export function launchModeLabel(owner?: object) {
857
- const browser = browserSettingsForOwner(owner);
858
- const managedBrowser = managedBrowserForOwner(owner);
859
- if (extensionsConfigured(owner)) {
860
- if (!isLocalDevToolsHost(browser.host)) return "invalid; extensions require a local endpoint";
861
- if (!browser.autoLaunchEnabled) return "invalid; extensions require auto-launch";
862
- if (managedBrowser && !managedBrowser.exited) {
863
- return managedBrowserPortConfigured(owner)
864
- ? "extension-owned browser on explicit port"
865
- : "extension-owned browser on dynamic port";
866
- }
867
- return managedBrowserPortConfigured(owner)
868
- ? "force extension-owned launch on explicit port"
869
- : "force extension-owned launch on dynamic port";
870
- }
871
- if (!isLocalDevToolsHost(browser.host)) return "manual remote endpoint";
872
- if (!browser.autoLaunchEnabled) return "manual; auto-launch disabled";
873
- if (managedBrowser && !managedBrowser.exited) {
874
- return managedBrowserPortConfigured(owner)
875
- ? "auto-launched on explicit port"
876
- : "auto-launched on dynamic port";
877
- }
878
- return managedBrowserPortConfigured(owner)
879
- ? "attach first; auto-launch explicit port"
880
- : "attach first; auto-launch dynamic port";
802
+ const browser = browserSettingsForOwner(owner);
803
+ const managedBrowser = managedBrowserForOwner(owner);
804
+ if (extensionsConfigured(owner)) {
805
+ if (!isLocalDevToolsHost(browser.host)) return "invalid; extensions require a local endpoint";
806
+ if (!browser.autoLaunchEnabled) return "invalid; extensions require auto-launch";
807
+ if (managedBrowser && !managedBrowser.exited) {
808
+ return managedBrowserPortConfigured(owner)
809
+ ? "extension-owned browser on explicit port"
810
+ : "extension-owned browser on dynamic port";
811
+ }
812
+ return managedBrowserPortConfigured(owner)
813
+ ? "force extension-owned launch on explicit port"
814
+ : "force extension-owned launch on dynamic port";
815
+ }
816
+ if (!isLocalDevToolsHost(browser.host)) return "manual remote endpoint";
817
+ if (!browser.autoLaunchEnabled) return "manual; auto-launch disabled";
818
+ if (managedBrowser && !managedBrowser.exited) {
819
+ return managedBrowserPortConfigured(owner) ? "auto-launched on explicit port" : "auto-launched on dynamic port";
820
+ }
821
+ return managedBrowserPortConfigured(owner)
822
+ ? "attach first; auto-launch explicit port"
823
+ : "attach first; auto-launch dynamic port";
881
824
  }
882
825
 
883
826
  export function launchAttemptLines(owner?: object) {
884
- const attempt = lastLaunchAttempt(owner);
885
- if (!attempt) return [];
827
+ const attempt = lastLaunchAttempt(owner);
828
+ if (!attempt) return [];
886
829
 
887
- const lines = [`Last launch attempt: ${attempt.mode}`];
888
- if (attempt.selectedCandidate) {
889
- lines.push(`Launched browser: ${attempt.selectedCandidate}`);
890
- } else {
891
- lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
892
- }
893
- if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
894
- if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
895
- return lines;
830
+ const lines = [`Last launch attempt: ${attempt.mode}`];
831
+ if (attempt.selectedCandidate) {
832
+ lines.push(`Launched browser: ${attempt.selectedCandidate}`);
833
+ } else {
834
+ lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
835
+ }
836
+ if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
837
+ if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
838
+ return lines;
896
839
  }
897
840
 
898
841
  export function launchHint(owner?: object) {
899
- const browser = browserSettingsForOwner(owner);
900
- if (extensionsConfigured(owner)) {
901
- return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
902
- }
903
- if (!isLocalDevToolsHost(browser.host)) {
904
- return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint(owner)}.`;
905
- }
906
- if (!browser.autoLaunchEnabled) {
907
- return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
908
- }
909
- const managedMode = managedBrowserPortConfigured(owner)
910
- ? `port ${managedBrowserPort(owner)}`
911
- : "a dynamic DevTools port";
912
- return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand(owner)}`;
842
+ const browser = browserSettingsForOwner(owner);
843
+ if (extensionsConfigured(owner)) {
844
+ return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
845
+ }
846
+ if (!isLocalDevToolsHost(browser.host)) {
847
+ return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint(owner)}.`;
848
+ }
849
+ if (!browser.autoLaunchEnabled) {
850
+ return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
851
+ }
852
+ const managedMode = managedBrowserPortConfigured(owner)
853
+ ? `port ${managedBrowserPort(owner)}`
854
+ : "a dynamic DevTools port";
855
+ return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand(owner)}`;
913
856
  }
914
857
 
915
858
  export function browserCandidateHint(owner?: object) {
916
- if (extensionsConfigured(owner)) {
917
- return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
918
- }
919
- return `Browser candidates: ${browserCandidateDefinitions(owner)
920
- .map((candidate) => candidate.label)
921
- .join(", ")}`;
859
+ if (extensionsConfigured(owner)) {
860
+ return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
861
+ }
862
+ return `Browser candidates: ${browserCandidateDefinitions(owner)
863
+ .map((candidate) => candidate.label)
864
+ .join(", ")}`;
922
865
  }
923
866
 
924
867
  export function chromeLaunchCommand(owner?: object) {
925
- const executable =
926
- browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
927
- const dataDir =
928
- process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
929
- return `${quoteCommandPart(executable)} --remote-debugging-port=${managedBrowserPort(owner)} --user-data-dir=${dataDir}`;
868
+ const executable = browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
869
+ const dataDir = process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
870
+ return `${quoteCommandPart(executable)} --remote-debugging-port=${managedBrowserPort(owner)} --user-data-dir=${dataDir}`;
930
871
  }
931
872
 
932
873
  function defaultManualBrowserExecutable() {
933
- return process.platform === "darwin"
934
- ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
935
- : process.platform === "win32"
936
- ? "chrome.exe"
937
- : "google-chrome";
874
+ return process.platform === "darwin"
875
+ ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
876
+ : process.platform === "win32"
877
+ ? "chrome.exe"
878
+ : "google-chrome";
938
879
  }
939
880
 
940
881
  export function quoteCommandPart(value: string) {
941
- return /\s/.test(value) ? `"${value.replaceAll('"', '\\"')}"` : value;
882
+ return /\s/.test(value) ? `"${value.replaceAll('"', '\\"')}"` : value;
942
883
  }
943
884
 
944
885
  export function endpointConfigHint() {
945
- return "Configure browser.endpoint, browser.executablePath, and browser.autoLaunch in pi-chrome-devtools.json or the /chrome-devtools Settings flow.";
886
+ return "Configure browser.endpoint, browser.executablePath, and browser.autoLaunch in pi-chrome-devtools.json or the /chrome-devtools Settings flow.";
946
887
  }
947
888
 
948
889
  export function isLocalDevToolsHost(host: string) {
949
- const normalizedHost = host.toLowerCase().replace(/^\[(.*)]$/, "$1");
950
- return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
890
+ const normalizedHost = host.toLowerCase().replace(/^\[(.*)]$/, "$1");
891
+ return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
951
892
  }
952
893
 
953
894
  function browserCandidateDefinitions(owner?: object): BrowserCandidateDefinition[] {
954
- const explicitCandidate = explicitBrowserCandidateDefinition(owner);
955
- if (explicitCandidate.length > 0) return explicitCandidate;
895
+ const explicitCandidate = explicitBrowserCandidateDefinition(owner);
896
+ if (explicitCandidate.length > 0) return explicitCandidate;
956
897
 
957
- return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
898
+ return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
958
899
  }
959
900
 
960
901
  function explicitBrowserCandidateDefinition(owner?: object): BrowserCandidateDefinition[] {
961
- const browser = browserSettingsForOwner(owner);
962
- if (!browser.executablePath) return [];
963
- return [
964
- {
965
- label:
966
- browser.executablePathSource === "environment"
967
- ? "PI_CHROME_DEVTOOLS_BROWSER"
968
- : "browser.executablePath",
969
- executable: browser.executablePath,
970
- source: "env",
971
- },
972
- ];
902
+ const browser = browserSettingsForOwner(owner);
903
+ if (!browser.executablePath) return [];
904
+ return [
905
+ {
906
+ label: browser.executablePathSource === "environment" ? "PI_CHROME_DEVTOOLS_BROWSER" : "browser.executablePath",
907
+ executable: browser.executablePath,
908
+ source: "env",
909
+ },
910
+ ];
973
911
  }
974
912
 
975
913
  function platformBrowserCandidateDefinitions(): BrowserCandidateDefinition[] {
976
- if (process.platform === "darwin") {
977
- return [
978
- {
979
- label: "Google Chrome",
980
- executable: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
981
- source: "wellKnownPath",
982
- },
983
- {
984
- label: "Chromium",
985
- executable: "/Applications/Chromium.app/Contents/MacOS/Chromium",
986
- source: "wellKnownPath",
987
- },
988
- {
989
- label: "Brave Browser",
990
- executable: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
991
- source: "wellKnownPath",
992
- },
993
- {
994
- label: "Microsoft Edge",
995
- executable: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
996
- source: "wellKnownPath",
997
- },
998
- ];
999
- }
1000
-
1001
- if (process.platform === "win32") {
1002
- return windowsBrowserCandidateDefinitions();
1003
- }
1004
-
1005
- return [
1006
- { label: "Google Chrome", executable: "google-chrome", source: "path" },
1007
- { label: "Google Chrome Stable", executable: "google-chrome-stable", source: "path" },
1008
- { label: "Chromium", executable: "chromium", source: "path" },
1009
- { label: "Chromium Browser", executable: "chromium-browser", source: "path" },
1010
- { label: "Brave Browser", executable: "brave-browser", source: "path" },
1011
- { label: "Brave", executable: "brave", source: "path" },
1012
- { label: "Microsoft Edge", executable: "microsoft-edge", source: "path" },
1013
- { label: "Microsoft Edge Stable", executable: "microsoft-edge-stable", source: "path" },
1014
- ];
914
+ if (process.platform === "darwin") {
915
+ return [
916
+ {
917
+ label: "Google Chrome",
918
+ executable: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
919
+ source: "wellKnownPath",
920
+ },
921
+ {
922
+ label: "Chromium",
923
+ executable: "/Applications/Chromium.app/Contents/MacOS/Chromium",
924
+ source: "wellKnownPath",
925
+ },
926
+ {
927
+ label: "Brave Browser",
928
+ executable: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
929
+ source: "wellKnownPath",
930
+ },
931
+ {
932
+ label: "Microsoft Edge",
933
+ executable: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
934
+ source: "wellKnownPath",
935
+ },
936
+ ];
937
+ }
938
+
939
+ if (process.platform === "win32") {
940
+ return windowsBrowserCandidateDefinitions();
941
+ }
942
+
943
+ return [
944
+ { label: "Google Chrome", executable: "google-chrome", source: "path" },
945
+ { label: "Google Chrome Stable", executable: "google-chrome-stable", source: "path" },
946
+ { label: "Chromium", executable: "chromium", source: "path" },
947
+ { label: "Chromium Browser", executable: "chromium-browser", source: "path" },
948
+ { label: "Brave Browser", executable: "brave-browser", source: "path" },
949
+ { label: "Brave", executable: "brave", source: "path" },
950
+ { label: "Microsoft Edge", executable: "microsoft-edge", source: "path" },
951
+ { label: "Microsoft Edge Stable", executable: "microsoft-edge-stable", source: "path" },
952
+ ];
1015
953
  }
1016
954
 
1017
955
  function windowsBrowserCandidateDefinitions(): BrowserCandidateDefinition[] {
1018
- const programFiles = [
1019
- process.env.PROGRAMFILES,
1020
- process.env["PROGRAMFILES(X86)"],
1021
- process.env.LOCALAPPDATA,
1022
- ].filter((value): value is string => typeof value === "string" && value.length > 0);
1023
- const wellKnownPaths = programFiles.flatMap((root) => [
1024
- join(root, "Google", "Chrome", "Application", "chrome.exe"),
1025
- join(root, "Chromium", "Application", "chrome.exe"),
1026
- join(root, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
1027
- join(root, "Microsoft", "Edge", "Application", "msedge.exe"),
1028
- ]);
1029
- return [
1030
- ...wellKnownPaths.map((executable) => ({
1031
- label: browserLabelFromExecutable(executable),
1032
- executable,
1033
- source: "wellKnownPath" as const,
1034
- })),
1035
- { label: "Google Chrome", executable: "chrome.exe", source: "path" },
1036
- { label: "Chromium", executable: "chromium.exe", source: "path" },
1037
- { label: "Brave Browser", executable: "brave.exe", source: "path" },
1038
- { label: "Microsoft Edge", executable: "msedge.exe", source: "path" },
1039
- ];
956
+ const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(
957
+ (value): value is string => typeof value === "string" && value.length > 0,
958
+ );
959
+ const wellKnownPaths = programFiles.flatMap((root) => [
960
+ join(root, "Google", "Chrome", "Application", "chrome.exe"),
961
+ join(root, "Chromium", "Application", "chrome.exe"),
962
+ join(root, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
963
+ join(root, "Microsoft", "Edge", "Application", "msedge.exe"),
964
+ ]);
965
+ return [
966
+ ...wellKnownPaths.map((executable) => ({
967
+ label: browserLabelFromExecutable(executable),
968
+ executable,
969
+ source: "wellKnownPath" as const,
970
+ })),
971
+ { label: "Google Chrome", executable: "chrome.exe", source: "path" },
972
+ { label: "Chromium", executable: "chromium.exe", source: "path" },
973
+ { label: "Brave Browser", executable: "brave.exe", source: "path" },
974
+ { label: "Microsoft Edge", executable: "msedge.exe", source: "path" },
975
+ ];
1040
976
  }
1041
977
 
1042
978
  function browserLabelFromExecutable(executable: string) {
1043
- const normalizedExecutable = normalizePathForComparison(executable);
1044
- if (normalizedExecutable.includes("brave")) return "Brave Browser";
1045
- if (normalizedExecutable.includes("edge") || normalizedExecutable.includes("msedge")) {
1046
- return "Microsoft Edge";
1047
- }
1048
- if (normalizedExecutable.includes("chromium")) return "Chromium";
1049
- return "Google Chrome";
979
+ const normalizedExecutable = normalizePathForComparison(executable);
980
+ if (normalizedExecutable.includes("brave")) return "Brave Browser";
981
+ if (normalizedExecutable.includes("edge") || normalizedExecutable.includes("msedge")) {
982
+ return "Microsoft Edge";
983
+ }
984
+ if (normalizedExecutable.includes("chromium")) return "Chromium";
985
+ return "Google Chrome";
1050
986
  }
1051
987
 
1052
988
  function uniqueBrowserCandidates(candidates: BrowserCandidateDefinition[]) {
1053
- const seen = new Set<string>();
1054
- return candidates.filter((candidate) => {
1055
- const key = normalizePathForComparison(candidate.executable);
1056
- if (seen.has(key)) return false;
1057
- seen.add(key);
1058
- return true;
1059
- });
989
+ const seen = new Set<string>();
990
+ return candidates.filter((candidate) => {
991
+ const key = normalizePathForComparison(candidate.executable);
992
+ if (seen.has(key)) return false;
993
+ seen.add(key);
994
+ return true;
995
+ });
1060
996
  }
1061
997
 
1062
998
  async function resolveBrowserCandidates(
1063
- definitions: BrowserCandidateDefinition[],
1064
- generation: number,
1065
- signal: AbortSignal,
1066
- owner?: object,
999
+ definitions: BrowserCandidateDefinition[],
1000
+ generation: number,
1001
+ signal: AbortSignal,
1002
+ owner?: object,
1067
1003
  ) {
1068
- const candidates: BrowserCandidate[] = [];
1069
- for (const definition of definitions) {
1070
- const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
1071
- throwIfBrowserLaunchCancelled(generation, signal, owner);
1072
- if (!resolvedExecutable) continue;
1073
- candidates.push({ ...definition, resolvedExecutable });
1074
- }
1075
- return uniqueBrowserCandidatesByResolvedPath(candidates);
1004
+ const candidates: BrowserCandidate[] = [];
1005
+ for (const definition of definitions) {
1006
+ const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
1007
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
1008
+ if (!resolvedExecutable) continue;
1009
+ candidates.push({ ...definition, resolvedExecutable });
1010
+ }
1011
+ return uniqueBrowserCandidatesByResolvedPath(candidates);
1076
1012
  }
1077
1013
 
1078
1014
  function uniqueBrowserCandidatesByResolvedPath(candidates: BrowserCandidate[]) {
1079
- const seen = new Set<string>();
1080
- return candidates.filter((candidate) => {
1081
- const key = normalizePathForComparison(resolve(candidate.resolvedExecutable));
1082
- if (seen.has(key)) return false;
1083
- seen.add(key);
1084
- return true;
1085
- });
1015
+ const seen = new Set<string>();
1016
+ return candidates.filter((candidate) => {
1017
+ const key = normalizePathForComparison(resolve(candidate.resolvedExecutable));
1018
+ if (seen.has(key)) return false;
1019
+ seen.add(key);
1020
+ return true;
1021
+ });
1086
1022
  }
1087
1023
 
1088
1024
  async function resolveBrowserExecutable(executable: string) {
1089
- if (hasPathSeparator(executable) || isAbsolute(executable)) {
1090
- const resolvedExecutable = isAbsolute(executable) ? executable : resolve(executable);
1091
- return (await canAccessExecutable(resolvedExecutable)) ? resolvedExecutable : undefined;
1092
- }
1025
+ if (hasPathSeparator(executable) || isAbsolute(executable)) {
1026
+ const resolvedExecutable = isAbsolute(executable) ? executable : resolve(executable);
1027
+ return (await canAccessExecutable(resolvedExecutable)) ? resolvedExecutable : undefined;
1028
+ }
1093
1029
 
1094
- for (const directory of executableSearchPath()) {
1095
- for (const executableName of executableSearchNames(executable)) {
1096
- const candidate = join(directory, executableName);
1097
- if (await canAccessExecutable(candidate)) return candidate;
1098
- }
1099
- }
1100
- return undefined;
1030
+ for (const directory of executableSearchPath()) {
1031
+ for (const executableName of executableSearchNames(executable)) {
1032
+ const candidate = join(directory, executableName);
1033
+ if (await canAccessExecutable(candidate)) return candidate;
1034
+ }
1035
+ }
1036
+ return undefined;
1101
1037
  }
1102
1038
 
1103
1039
  function hasPathSeparator(path: string) {
1104
- return path.includes("/") || path.includes("\\");
1040
+ return path.includes("/") || path.includes("\\");
1105
1041
  }
1106
1042
 
1107
1043
  function executableSearchPath() {
1108
- return (process.env.PATH ?? "").split(delimiter).filter((part) => part.length > 0);
1044
+ return (process.env.PATH ?? "").split(delimiter).filter((part) => part.length > 0);
1109
1045
  }
1110
1046
 
1111
1047
  function executableSearchNames(executable: string) {
1112
- if (process.platform !== "win32" || /\.[a-z0-9]+$/i.test(executable)) return [executable];
1113
- return [executable, `${executable}.exe`, `${executable}.cmd`, `${executable}.bat`];
1048
+ if (process.platform !== "win32" || /\.[a-z0-9]+$/i.test(executable)) return [executable];
1049
+ return [executable, `${executable}.exe`, `${executable}.cmd`, `${executable}.bat`];
1114
1050
  }
1115
1051
 
1116
1052
  async function canAccessExecutable(path: string) {
1117
- try {
1118
- await browserManagerOperations.access(
1119
- path,
1120
- process.platform === "win32" ? constants.F_OK : constants.X_OK,
1121
- );
1122
- return true;
1123
- } catch {
1124
- return false;
1125
- }
1053
+ try {
1054
+ await browserManagerOperations.access(path, process.platform === "win32" ? constants.F_OK : constants.X_OK);
1055
+ return true;
1056
+ } catch {
1057
+ return false;
1058
+ }
1126
1059
  }
1127
1060
 
1128
1061
  function formatBrowserCandidate(candidate: BrowserCandidate) {
1129
- return `${candidate.label} (${candidate.resolvedExecutable})`;
1062
+ return `${candidate.label} (${candidate.resolvedExecutable})`;
1130
1063
  }
1131
1064
 
1132
1065
  function formatBrowserCandidateDefinition(candidate: BrowserCandidateDefinition) {
1133
- return `${candidate.label} (${candidate.executable})`;
1066
+ return `${candidate.label} (${candidate.executable})`;
1134
1067
  }
1135
1068
 
1136
- function noBrowserCandidateMessage(
1137
- candidateDefinitions: BrowserCandidateDefinition[],
1138
- owner?: object,
1139
- ) {
1140
- if (extensionsConfigured(owner)) {
1141
- return [
1142
- "Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
1143
- `Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
1144
- "Set browser.executablePath to an executable Chrome for Testing or Chromium binary.",
1145
- ].join("\n");
1146
- }
1147
- return [
1148
- "Cannot auto-launch Chrome DevTools because no Chromium-family browser executable was found.",
1149
- `Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
1150
- endpointConfigHint(),
1151
- ].join("\n");
1069
+ function noBrowserCandidateMessage(candidateDefinitions: BrowserCandidateDefinition[], owner?: object) {
1070
+ if (extensionsConfigured(owner)) {
1071
+ return [
1072
+ "Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
1073
+ `Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
1074
+ "Set browser.executablePath to an executable Chrome for Testing or Chromium binary.",
1075
+ ].join("\n");
1076
+ }
1077
+ return [
1078
+ "Cannot auto-launch Chrome DevTools because no Chromium-family browser executable was found.",
1079
+ `Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
1080
+ endpointConfigHint(),
1081
+ ].join("\n");
1152
1082
  }
1153
1083
 
1154
1084
  export function formatPageListItem(page: DevToolsPage) {
1155
- return `- ${page.id}: ${page.title || "(untitled)"} ${page.url}`;
1085
+ return `- ${page.id}: ${page.title || "(untitled)"} ${page.url}`;
1156
1086
  }
1157
1087
 
1158
1088
  function abortableSleep(ms: number, signal?: AbortSignal) {
1159
- return new Promise<void>((resolveSleep, reject) => {
1160
- if (signal?.aborted) {
1161
- reject(signal.reason);
1162
- return;
1163
- }
1164
- const timeout = setTimeout(settleResolve, ms);
1165
- const onAbort = () => settleReject(signal?.reason);
1166
- function cleanup() {
1167
- clearTimeout(timeout);
1168
- signal?.removeEventListener("abort", onAbort);
1169
- }
1170
- function settleResolve() {
1171
- cleanup();
1172
- resolveSleep();
1173
- }
1174
- function settleReject(reason: unknown) {
1175
- cleanup();
1176
- reject(reason);
1177
- }
1178
- signal?.addEventListener("abort", onAbort, { once: true });
1179
- });
1089
+ return new Promise<void>((resolveSleep, reject) => {
1090
+ if (signal?.aborted) {
1091
+ reject(signal.reason);
1092
+ return;
1093
+ }
1094
+ const timeout = setTimeout(settleResolve, ms);
1095
+ const onAbort = () => settleReject(signal?.reason);
1096
+ function cleanup() {
1097
+ clearTimeout(timeout);
1098
+ signal?.removeEventListener("abort", onAbort);
1099
+ }
1100
+ function settleResolve() {
1101
+ cleanup();
1102
+ resolveSleep();
1103
+ }
1104
+ function settleReject(reason: unknown) {
1105
+ cleanup();
1106
+ reject(reason);
1107
+ }
1108
+ signal?.addEventListener("abort", onAbort, { once: true });
1109
+ });
1180
1110
  }
1181
1111
 
1182
1112
  function inspectBrowserVersion(executable: string, signal: AbortSignal) {
1183
- return new Promise<string>((resolveVersion, reject) => {
1184
- execFile(executable, ["--version"], { signal, timeout: 5_000 }, (error, stdout, stderr) => {
1185
- if (error) {
1186
- reject(error);
1187
- return;
1188
- }
1189
- resolveVersion(`${stdout}${stderr}`.trim());
1190
- });
1191
- });
1113
+ return new Promise<string>((resolveVersion, reject) => {
1114
+ execFile(executable, ["--version"], { signal, timeout: 5_000 }, (error, stdout, stderr) => {
1115
+ if (error) {
1116
+ reject(error);
1117
+ return;
1118
+ }
1119
+ resolveVersion(`${stdout}${stderr}`.trim());
1120
+ });
1121
+ });
1192
1122
  }
1193
1123
 
1194
1124
  function isPortAvailable(host: string, port: number) {
1195
- return new Promise<boolean>((resolveAvailability, reject) => {
1196
- const server = createServer();
1197
- const normalizedHost = host.replace(/^\[(.*)]$/, "$1");
1198
- server.unref();
1199
- server.once("error", (error: NodeJS.ErrnoException) => {
1200
- if (error.code === "EADDRINUSE") resolveAvailability(false);
1201
- else reject(error);
1202
- });
1203
- server.listen(port, normalizedHost, () => {
1204
- server.close((error) => {
1205
- if (error) reject(error);
1206
- else resolveAvailability(true);
1207
- });
1208
- });
1209
- });
1125
+ return new Promise<boolean>((resolveAvailability, reject) => {
1126
+ const server = createServer();
1127
+ const normalizedHost = host.replace(/^\[(.*)]$/, "$1");
1128
+ server.unref();
1129
+ server.once("error", (error: NodeJS.ErrnoException) => {
1130
+ if (error.code === "EADDRINUSE") resolveAvailability(false);
1131
+ else reject(error);
1132
+ });
1133
+ server.listen(port, normalizedHost, () => {
1134
+ server.close((error) => {
1135
+ if (error) reject(error);
1136
+ else resolveAvailability(true);
1137
+ });
1138
+ });
1139
+ });
1210
1140
  }
1211
1141
 
1212
1142
  class DevToolsEndpointError extends Error {
1213
- readonly retryable: boolean;
1214
- readonly launchable: boolean;
1215
-
1216
- constructor(message: string, options: { retryable?: boolean; launchable?: boolean } = {}) {
1217
- super(message);
1218
- this.name = "DevToolsEndpointError";
1219
- this.retryable = options.retryable ?? false;
1220
- this.launchable = options.launchable ?? false;
1221
- }
1143
+ readonly retryable: boolean;
1144
+ readonly launchable: boolean;
1145
+
1146
+ constructor(message: string, options: { retryable?: boolean; launchable?: boolean } = {}) {
1147
+ super(message);
1148
+ this.name = "DevToolsEndpointError";
1149
+ this.retryable = options.retryable ?? false;
1150
+ this.launchable = options.launchable ?? false;
1151
+ }
1222
1152
  }