@narumitw/pi-chrome-devtools 0.52.3 → 0.53.1

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 (35) hide show
  1. package/README.md +62 -9
  2. package/dist/chunks/{browser-settings-menu-VWCNG4HB.js → browser-settings-menu-REIU7C3Q.js} +83 -7
  3. package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +7 -0
  4. package/dist/chunks/chunk-GTTYS5GT.js +384 -0
  5. package/dist/chunks/chunk-GTTYS5GT.js.map +7 -0
  6. package/dist/chunks/{chunk-RH6KESL3.js → chunk-SRT3UUIP.js} +473 -574
  7. package/dist/chunks/chunk-SRT3UUIP.js.map +7 -0
  8. package/dist/chunks/chunk-YJVYJ2L2.js +469 -0
  9. package/dist/chunks/chunk-YJVYJ2L2.js.map +7 -0
  10. package/dist/chunks/{menu-PLD3S3EO.js → menu-H5HMVGHI.js} +72 -39
  11. package/dist/chunks/menu-H5HMVGHI.js.map +7 -0
  12. package/dist/chunks/tools-6P7EUX37.js +964 -0
  13. package/dist/chunks/tools-6P7EUX37.js.map +7 -0
  14. package/dist/index.ts +166 -199
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +408 -169
  18. package/src/browser-settings-menu.ts +100 -5
  19. package/src/cdp-client.ts +347 -90
  20. package/src/chrome-devtools.ts +58 -11
  21. package/src/lazy-tools.ts +37 -4
  22. package/src/menu.ts +80 -39
  23. package/src/render.ts +12 -1
  24. package/src/runtime.ts +92 -0
  25. package/src/settings.ts +47 -10
  26. package/src/tool-names.ts +16 -1
  27. package/src/tool-selector.ts +85 -44
  28. package/src/tools.ts +91 -15
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +409 -0
  31. package/src/webmcp/protocol.ts +426 -0
  32. package/src/webmcp/tools.ts +190 -0
  33. package/dist/chunks/browser-settings-menu-VWCNG4HB.js.map +0 -7
  34. package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
  35. package/dist/chunks/menu-PLD3S3EO.js.map +0 -7
@@ -8,16 +8,21 @@ import {
8
8
  BROWSER_SHUTDOWN_WAIT_MS,
9
9
  type BrowserCandidate,
10
10
  type BrowserCandidateDefinition,
11
+ type BrowserLaunchAttempt,
11
12
  DEFAULT_ENDPOINT_RETRY_MS,
12
13
  DEFAULT_ENDPOINT_WAIT_MS,
13
14
  DEFAULT_HTTP_TIMEOUT_MS,
14
15
  DEVTOOLS_ACTIVE_PORT_FILE,
15
16
  type DevToolsPage,
17
+ invalidateWebMcpOperations,
16
18
  MANAGED_BROWSER_PROFILE_PREFIX,
17
19
  type ManagedBrowser,
18
20
  state,
19
21
  } from "./runtime.js";
22
+ import type { EffectiveBrowserSettings } from "./settings.js";
20
23
 
24
+ // Endpoint attachment, managed launch, dynamic-port publication, and shutdown intentionally remain
25
+ // together because they form one ownership state machine behind the testable operations boundary.
21
26
  interface BrowserSpawnOptions {
22
27
  shell: false;
23
28
  stdio: "ignore";
@@ -49,6 +54,129 @@ const DEFAULT_BROWSER_MANAGER_OPERATIONS: BrowserManagerOperations = {
49
54
 
50
55
  let browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
51
56
 
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;
65
+ }
66
+
67
+ const managedBrowserScopes = new WeakMap<object, ManagedBrowserScope>();
68
+
69
+ function cloneBrowserSettings(browser: EffectiveBrowserSettings): EffectiveBrowserSettings {
70
+ return { ...browser, extensionPaths: [...browser.extensionPaths] };
71
+ }
72
+
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
+ };
88
+ }
89
+
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;
102
+ }
103
+
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;
110
+ }
111
+
112
+ export function syncManagedBrowserSettings(owner: object, browser: EffectiveBrowserSettings) {
113
+ const scope = managedBrowserScope(owner);
114
+ scope.browser = cloneBrowserSettings(browser);
115
+ scope.port = browser.port;
116
+ }
117
+
118
+ export function browserSettingsForOwner(owner?: object) {
119
+ return owner ? managedBrowserScope(owner).browser : runtimeBrowserSettings();
120
+ }
121
+
122
+ export function managedBrowserExtensionPaths(owner?: object) {
123
+ return browserSettingsForOwner(owner).extensionPaths;
124
+ }
125
+
126
+ export function managedBrowserExtensionPathsSource(owner?: object) {
127
+ return browserSettingsForOwner(owner).extensionPathsSource;
128
+ }
129
+
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;
135
+ }
136
+
137
+ function setManagedBrowser(owner: object | undefined, browser: ManagedBrowser | undefined) {
138
+ if (owner) managedBrowserScope(owner).managedBrowser = browser;
139
+ else state.managedBrowser = browser;
140
+ }
141
+
142
+ function launchPromiseForOwner(owner?: object) {
143
+ return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
144
+ }
145
+
146
+ function setLaunchPromise(owner: object | undefined, launchPromise: Promise<void> | undefined) {
147
+ if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
148
+ else state.launchPromise = launchPromise;
149
+ }
150
+
151
+ function managedBrowserPort(owner?: object) {
152
+ return owner ? managedBrowserScope(owner).port : state.port;
153
+ }
154
+
155
+ function managedBrowserConfiguredPort(owner?: object) {
156
+ return browserSettingsForOwner(owner).port;
157
+ }
158
+
159
+ function managedBrowserPortConfigured(owner?: object) {
160
+ return browserSettingsForOwner(owner).portConfigured;
161
+ }
162
+
163
+ function setManagedBrowserPort(owner: object | undefined, port: number) {
164
+ if (owner) managedBrowserScope(owner).port = port;
165
+ else state.port = port;
166
+ }
167
+
168
+ function lastLaunchAttempt(owner?: object) {
169
+ return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
170
+ }
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;
178
+ }
179
+
52
180
  export function setBrowserManagerOperationsForTests(overrides: Partial<BrowserManagerOperations>) {
53
181
  const previous = browserManagerOperations;
54
182
  browserManagerOperations = { ...DEFAULT_BROWSER_MANAGER_OPERATIONS, ...overrides };
@@ -69,19 +197,28 @@ function normalizePathForComparison(value: string) {
69
197
  return process.platform === "win32" ? value.toLowerCase() : value;
70
198
  }
71
199
 
72
- export async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS) {
73
- if (extensionsConfigured()) {
74
- validateExtensionLaunchMode();
75
- await ensureManagedBrowserLaunched(waitMs);
200
+ export async function ensureDevToolsEndpoint(
201
+ waitMs = DEFAULT_ENDPOINT_WAIT_MS,
202
+ callerSignal?: AbortSignal,
203
+ owner?: object,
204
+ ) {
205
+ callerSignal?.throwIfAborted();
206
+ if (extensionsConfigured(owner)) {
207
+ validateExtensionLaunchMode(owner);
208
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
76
209
  return;
77
210
  }
78
- if (canAutoLaunchBrowser()) {
211
+ if (canAutoLaunchBrowser(owner)) {
79
212
  try {
80
- await withEndpointRetry(() => fetchDevToolsJson<unknown>("/json/version"), waitMs);
213
+ await withEndpointRetry(
214
+ () => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
215
+ waitMs,
216
+ callerSignal,
217
+ );
81
218
  return;
82
219
  } catch (error) {
83
- if (shouldAutoLaunchAfterEndpointError(error)) {
84
- await ensureManagedBrowserLaunched(waitMs);
220
+ if (shouldAutoLaunchAfterEndpointError(error, owner)) {
221
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
85
222
  return;
86
223
  }
87
224
  throw error;
@@ -89,120 +226,175 @@ export async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS)
89
226
  }
90
227
 
91
228
  try {
92
- await fetchDevToolsJson<unknown>("/json/version");
229
+ await fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner);
93
230
  } catch (error) {
94
231
  if (isRetryableEndpointError(error)) return;
95
232
  throw error;
96
233
  }
97
234
  }
98
235
 
99
- function validateExtensionLaunchMode() {
100
- if (!isLocalDevToolsHost(state.host)) {
236
+ function validateExtensionLaunchMode(owner?: object) {
237
+ const browser = browserSettingsForOwner(owner);
238
+ if (!isLocalDevToolsHost(browser.host)) {
101
239
  throw new DevToolsEndpointError(
102
240
  "Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified.",
103
241
  );
104
242
  }
105
- if (!state.autoLaunchEnabled) {
243
+ if (!browser.autoLaunchEnabled) {
106
244
  throw new DevToolsEndpointError(
107
245
  "Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser.",
108
246
  );
109
247
  }
110
- if (!state.browserExecutable) {
248
+ if (!browser.executablePath) {
111
249
  throw new DevToolsEndpointError(
112
250
  "Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json.",
113
251
  );
114
252
  }
115
253
  }
116
254
 
117
- async function ensureManagedBrowserLaunched(waitMs: number) {
118
- if (state.launchPromise) return state.launchPromise;
119
- if (state.managedBrowser && !state.managedBrowser.exited && state.managedBrowser.ready) return;
120
- const generation = state.sessionGeneration;
121
- const signal = state.sessionController.signal;
122
- throwIfBrowserLaunchCancelled(generation, signal);
123
-
124
- const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal);
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);
125
274
  const wrappedPromise = launchPromise.finally(() => {
126
- if (state.launchPromise === wrappedPromise) state.launchPromise = undefined;
275
+ if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, undefined);
276
+ });
277
+ setLaunchPromise(owner, wrappedPromise);
278
+ await waitForCaller(wrappedPromise, callerSignal);
279
+ }
280
+
281
+ 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
+ );
127
298
  });
128
- state.launchPromise = wrappedPromise;
129
- return wrappedPromise;
130
299
  }
131
300
 
132
301
  async function prepareManagedBrowserLaunch(
133
302
  waitMs: number,
134
303
  generation: number,
135
304
  signal: AbortSignal,
305
+ owner?: object,
136
306
  ) {
137
- if (state.managedBrowser) {
138
- await shutdownManagedBrowser(state.managedBrowser, { awaitLaunch: false });
139
- throwIfBrowserLaunchCancelled(generation, signal);
307
+ const existingBrowser = managedBrowserForOwner(owner);
308
+ if (existingBrowser) {
309
+ await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
310
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
140
311
  }
141
- if (state.portConfigured) {
142
- const available = await browserManagerOperations.isPortAvailable(state.host, state.port);
143
- throwIfBrowserLaunchCancelled(generation, signal);
312
+ if (managedBrowserPortConfigured(owner)) {
313
+ const available = await browserManagerOperations.isPortAvailable(
314
+ browserSettingsForOwner(owner).host,
315
+ managedBrowserPort(owner),
316
+ );
317
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
144
318
  if (!available) {
145
319
  throw new DevToolsEndpointError(
146
- `Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
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.`,
147
321
  );
148
322
  }
149
323
  }
150
- return launchManagedBrowser(waitMs, generation, signal);
324
+ return launchManagedBrowser(waitMs, generation, signal, owner);
151
325
  }
152
326
 
153
- async function launchManagedBrowser(waitMs: number, generation: number, signal: AbortSignal) {
154
- throwIfBrowserLaunchCancelled(generation, signal);
155
- const candidateDefinitions = browserCandidateDefinitions();
156
- const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal);
157
- throwIfBrowserLaunchCancelled(generation, signal);
158
- state.lastLaunchAttempt = {
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, {
159
343
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
160
- mode: state.portConfigured ? "explicit-port" : "dynamic-port",
161
- };
344
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
345
+ });
162
346
 
163
347
  if (candidates.length === 0) {
164
- throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
348
+ throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
165
349
  }
166
350
 
167
351
  let lastError: unknown;
168
352
  for (const candidate of candidates) {
169
- throwIfBrowserLaunchCancelled(generation, signal);
353
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
170
354
  try {
171
- if (extensionsConfigured())
172
- await requireSupportedExtensionBrowser(candidate, generation, signal);
173
- await launchBrowserCandidate(candidate, waitMs, generation, signal);
174
- throwIfBrowserLaunchCancelled(generation, signal);
175
- state.lastLaunchAttempt = {
176
- ...state.lastLaunchAttempt,
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",
177
363
  selectedCandidate: formatBrowserCandidate(candidate),
178
- userDataDir: state.managedBrowser?.userDataDir,
179
- };
364
+ userDataDir: managedBrowserForOwner(owner)?.userDataDir,
365
+ });
180
366
  return;
181
367
  } catch (error) {
182
368
  try {
183
- throwIfBrowserLaunchCancelled(generation, signal);
369
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
184
370
  } catch (cancellation) {
185
- state.lastLaunchAttempt = {
186
- ...state.lastLaunchAttempt,
371
+ setLastLaunchAttempt(owner, {
372
+ ...(lastLaunchAttempt(owner) ?? {
373
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
374
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
375
+ }),
187
376
  lastError: formatError(cancellation),
188
- };
377
+ });
189
378
  throw cancellation;
190
379
  }
191
380
  lastError = error;
192
- state.lastLaunchAttempt = {
193
- ...state.lastLaunchAttempt,
381
+ setLastLaunchAttempt(owner, {
382
+ ...(lastLaunchAttempt(owner) ?? {
383
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
384
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
385
+ }),
194
386
  lastError: formatError(error),
195
- };
387
+ });
196
388
  }
197
389
  }
198
390
 
199
- if (lastError instanceof DevToolsEndpointError && extensionsConfigured()) throw lastError;
391
+ if (lastError instanceof DevToolsEndpointError && extensionsConfigured(owner)) throw lastError;
200
392
  throw new DevToolsEndpointError(
201
393
  [
202
394
  "Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
203
395
  `Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
204
396
  lastError ? `Last error: ${formatError(lastError)}` : undefined,
205
- launchHint(),
397
+ launchHint(owner),
206
398
  endpointConfigHint(),
207
399
  ]
208
400
  .filter(Boolean)
@@ -214,6 +406,7 @@ async function requireSupportedExtensionBrowser(
214
406
  candidate: BrowserCandidate,
215
407
  generation: number,
216
408
  signal: AbortSignal,
409
+ owner?: object,
217
410
  ) {
218
411
  let version: string;
219
412
  try {
@@ -221,12 +414,12 @@ async function requireSupportedExtensionBrowser(
221
414
  await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)
222
415
  ).trim();
223
416
  } catch (error) {
224
- throwIfBrowserLaunchCancelled(generation, signal);
417
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
225
418
  throw new DevToolsEndpointError(
226
419
  `Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError(error)}. Configure Chrome for Testing or Chromium.`,
227
420
  );
228
421
  }
229
- throwIfBrowserLaunchCancelled(generation, signal);
422
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
230
423
  const classification = classifyExtensionBrowserVersion(version);
231
424
  if (!classification.supported) {
232
425
  throw new DevToolsEndpointError(
@@ -252,19 +445,22 @@ async function launchBrowserCandidate(
252
445
  waitMs: number,
253
446
  generation: number,
254
447
  signal: AbortSignal,
448
+ owner?: object,
255
449
  ) {
256
- throwIfBrowserLaunchCancelled(generation, signal);
450
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
257
451
  const userDataDir = await browserManagerOperations.mkdtemp(
258
452
  join(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX),
259
453
  );
260
454
  let managedBrowser: ManagedBrowser | undefined;
261
455
  try {
262
- throwIfBrowserLaunchCancelled(generation, signal);
263
- const portArgument = state.portConfigured ? String(state.port) : "0";
456
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
457
+ const portArgument = managedBrowserPortConfigured(owner)
458
+ ? String(managedBrowserPort(owner))
459
+ : "0";
264
460
  const args = buildManagedBrowserLaunchArguments(
265
461
  userDataDir,
266
462
  portArgument,
267
- state.extensionPaths,
463
+ managedBrowserExtensionPaths(owner),
268
464
  );
269
465
  const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
270
466
  shell: false,
@@ -276,22 +472,29 @@ async function launchBrowserCandidate(
276
472
  exited: false,
277
473
  ready: false,
278
474
  ownerGeneration: generation,
475
+ ...(owner ? { sessionOwner: owner } : {}),
279
476
  };
280
477
  managedBrowser = launchedBrowser;
281
- state.managedBrowser = launchedBrowser;
478
+ setManagedBrowser(owner, launchedBrowser);
282
479
 
283
480
  child.once("exit", () => {
481
+ if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
482
+ invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
483
+ }
284
484
  launchedBrowser.exited = true;
285
485
  launchedBrowser.ready = false;
286
- if (!state.portConfigured && launchedBrowser.port === state.port) {
287
- state.port = state.configuredPort;
486
+ if (
487
+ !managedBrowserPortConfigured(owner) &&
488
+ launchedBrowser.port === managedBrowserPort(owner)
489
+ ) {
490
+ setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
288
491
  }
289
492
  });
290
493
 
291
494
  await waitForBrowserSpawn(child);
292
- throwIfBrowserLaunchCancelled(generation, signal);
293
- if (state.portConfigured) {
294
- launchedBrowser.port = state.port;
495
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
496
+ if (managedBrowserPortConfigured(owner)) {
497
+ launchedBrowser.port = managedBrowserPort(owner);
295
498
  } else {
296
499
  launchedBrowser.port = await readManagedBrowserPort(
297
500
  userDataDir,
@@ -299,15 +502,16 @@ async function launchBrowserCandidate(
299
502
  waitMs,
300
503
  generation,
301
504
  signal,
505
+ owner,
302
506
  );
303
- throwIfBrowserLaunchCancelled(generation, signal);
304
- state.port = launchedBrowser.port;
507
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
508
+ setManagedBrowserPort(owner, launchedBrowser.port);
305
509
  }
306
- await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
307
- throwIfBrowserLaunchCancelled(generation, signal);
510
+ await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
511
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
308
512
  launchedBrowser.ready = true;
309
513
  } catch (error) {
310
- if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
514
+ if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
311
515
  else {
312
516
  await browserManagerOperations
313
517
  .rm(userDataDir, { recursive: true, force: true })
@@ -362,6 +566,7 @@ async function readManagedBrowserPort(
362
566
  waitMs: number,
363
567
  generation: number,
364
568
  signal: AbortSignal,
569
+ owner?: object,
365
570
  ) {
366
571
  const activePortFile = join(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
367
572
  const deadline = Date.now() + waitMs;
@@ -373,7 +578,7 @@ async function readManagedBrowserPort(
373
578
  if (isNodeError(error) && error.code === "ENOENT") return undefined;
374
579
  throw error;
375
580
  });
376
- throwIfBrowserLaunchCancelled(generation, signal);
581
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
377
582
  const portText = text?.split(/\r?\n/, 1)[0]?.trim();
378
583
  const port = Number(portText);
379
584
  if (Number.isInteger(port) && port > 0) return port;
@@ -384,7 +589,7 @@ async function readManagedBrowserPort(
384
589
  [
385
590
  "Timed out waiting for auto-launched browser DevToolsActivePort.",
386
591
  `Expected file: ${activePortFile}`,
387
- launchHint(),
592
+ launchHint(owner),
388
593
  ].join("\n"),
389
594
  );
390
595
  }
@@ -397,25 +602,26 @@ async function waitForDevToolsEndpoint(
397
602
  managedBrowser: ManagedBrowser,
398
603
  generation: number,
399
604
  signal: AbortSignal,
605
+ owner?: object,
400
606
  ) {
401
607
  const deadline = Date.now() + waitMs;
402
608
  while (true) {
403
609
  throwIfManagedBrowserExited(managedBrowser);
404
610
  try {
405
- await fetchDevToolsJson<unknown>("/json/version", { signal });
406
- throwIfBrowserLaunchCancelled(generation, signal);
611
+ await fetchDevToolsJson<unknown>("/json/version", { signal }, owner);
612
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
407
613
  return;
408
614
  } catch (error) {
409
615
  if (!isRetryableEndpointError(error)) throw error;
410
616
  }
411
617
 
412
- throwIfBrowserLaunchCancelled(generation, signal);
618
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
413
619
  const remainingMs = deadline - Date.now();
414
620
  if (remainingMs <= 0) {
415
621
  throw new DevToolsEndpointError(
416
622
  [
417
- `Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
418
- launchHint(),
623
+ `Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
624
+ launchHint(owner),
419
625
  ].join("\n"),
420
626
  );
421
627
  }
@@ -428,36 +634,52 @@ function throwIfManagedBrowserExited(managedBrowser: ManagedBrowser) {
428
634
  throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
429
635
  }
430
636
 
431
- function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal) {
432
- if (!state.shuttingDown && generation === state.sessionGeneration && !signal.aborted) {
433
- return;
434
- }
637
+ 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;
435
641
  throw new DevToolsEndpointError(
436
- generation === state.sessionGeneration
642
+ generation === currentGeneration
437
643
  ? "Chrome DevTools browser launch cancelled during shutdown."
438
644
  : "Chrome DevTools browser launch cancelled because the session was replaced.",
439
645
  );
440
646
  }
441
647
 
442
648
  export async function shutdownManagedBrowser(
443
- managedBrowser = state.managedBrowser,
444
- options: { awaitLaunch?: boolean; cancelLaunch?: boolean } = {},
649
+ managedBrowser?: ManagedBrowser,
650
+ options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
445
651
  ) {
446
- if (options.cancelLaunch) state.shuttingDown = true;
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);
447
665
  if (options.awaitLaunch !== false) {
448
- await state.launchPromise?.catch(() => undefined);
449
- managedBrowser = managedBrowser ?? state.managedBrowser;
666
+ await launchPromiseForOwner(owner)?.catch(() => undefined);
667
+ managedBrowser ??= managedBrowserForOwner(owner);
450
668
  }
451
669
  if (!managedBrowser) return;
452
670
  if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
453
671
 
454
- const cleanup = cleanupManagedBrowser(managedBrowser);
672
+ const cleanup = cleanupManagedBrowser(managedBrowser, owner);
455
673
  managedBrowser.cleanupPromise = cleanup;
456
674
  return cleanup;
457
675
  }
458
676
 
459
- async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
460
- if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
677
+ 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
+ }
461
683
  if (!managedBrowser.exited) {
462
684
  killManagedBrowserProcess(managedBrowser);
463
685
  await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
@@ -470,8 +692,8 @@ async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
470
692
  await browserManagerOperations
471
693
  .rm(managedBrowser.userDataDir, { recursive: true, force: true })
472
694
  .catch(() => undefined);
473
- if (!state.portConfigured && managedBrowser.port === state.port) {
474
- state.port = state.configuredPort;
695
+ if (!managedBrowserPortConfigured(owner) && managedBrowser.port === managedBrowserPort(owner)) {
696
+ setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
475
697
  }
476
698
  }
477
699
 
@@ -505,8 +727,8 @@ function waitForManagedBrowserExit(managedBrowser: ManagedBrowser, waitMs: numbe
505
727
  });
506
728
  }
507
729
 
508
- export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
509
- const url = `${devToolsEndpoint()}${path}`;
730
+ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit, owner?: object) {
731
+ const url = `${devToolsEndpoint(owner)}${path}`;
510
732
  let response: Response;
511
733
  try {
512
734
  const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
@@ -515,7 +737,7 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
515
737
  signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal,
516
738
  });
517
739
  } catch (error) {
518
- throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
740
+ throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
519
741
  launchable: true,
520
742
  retryable: true,
521
743
  });
@@ -545,9 +767,14 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
545
767
  }
546
768
  }
547
769
 
548
- export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs: number) {
770
+ export async function withEndpointRetry<T>(
771
+ operation: () => Promise<T>,
772
+ waitMs: number,
773
+ signal?: AbortSignal,
774
+ ) {
549
775
  const deadline = Date.now() + waitMs;
550
776
  while (true) {
777
+ signal?.throwIfAborted();
551
778
  try {
552
779
  return await operation();
553
780
  } catch (error) {
@@ -556,7 +783,10 @@ export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs:
556
783
  const remainingMs = deadline - Date.now();
557
784
  if (remainingMs <= 0) throw error;
558
785
 
559
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs));
786
+ await browserManagerOperations.sleep(
787
+ Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
788
+ signal,
789
+ );
560
790
  }
561
791
  }
562
792
  }
@@ -569,28 +799,29 @@ function isLaunchableEndpointError(error: unknown) {
569
799
  return error instanceof DevToolsEndpointError && error.launchable;
570
800
  }
571
801
 
572
- function shouldAutoLaunchAfterEndpointError(error: unknown) {
573
- if (!canAutoLaunchBrowser()) return false;
802
+ function shouldAutoLaunchAfterEndpointError(error: unknown, owner?: object) {
803
+ if (!canAutoLaunchBrowser(owner)) return false;
574
804
  if (isLaunchableEndpointError(error)) return true;
575
805
 
576
806
  // After the attach-first attempt (including its retry window, when applicable) fails, treat any
577
807
  // DevTools endpoint error on an unpinned port as a conflict we can avoid with a dynamic port.
578
- return !state.portConfigured && error instanceof DevToolsEndpointError;
808
+ return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
579
809
  }
580
810
 
581
- function canAutoLaunchBrowser() {
582
- return state.autoLaunchEnabled && isLocalDevToolsHost(state.host);
811
+ function canAutoLaunchBrowser(owner?: object) {
812
+ const browser = browserSettingsForOwner(owner);
813
+ return browser.autoLaunchEnabled && isLocalDevToolsHost(browser.host);
583
814
  }
584
815
 
585
- function extensionsConfigured() {
586
- return state.extensionPaths.length > 0;
816
+ function extensionsConfigured(owner?: object) {
817
+ return managedBrowserExtensionPaths(owner).length > 0;
587
818
  }
588
819
 
589
- function endpointConnectionErrorMessage(error: unknown) {
820
+ function endpointConnectionErrorMessage(error: unknown, owner?: object) {
590
821
  const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
591
822
  return [
592
- `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
593
- launchHint(),
823
+ `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
824
+ launchHint(owner),
594
825
  endpointConfigHint(),
595
826
  ].join("\n");
596
827
  }
@@ -599,8 +830,8 @@ function isTimeoutError(error: unknown) {
599
830
  return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
600
831
  }
601
832
 
602
- export function devToolsEndpoint() {
603
- return `http://${formatHostForUrl(state.host)}:${state.port}`;
833
+ export function devToolsEndpoint(owner?: object) {
834
+ return `http://${formatHostForUrl(browserSettingsForOwner(owner).host)}:${managedBrowserPort(owner)}`;
604
835
  }
605
836
 
606
837
  export function formatHostForUrl(host: string) {
@@ -608,91 +839,94 @@ export function formatHostForUrl(host: string) {
608
839
  return host.includes(":") ? `[${host}]` : host;
609
840
  }
610
841
 
611
- export function endpointSourceLabel() {
612
- return state.endpointSource;
842
+ export function endpointSourceLabel(owner?: object) {
843
+ return browserSettingsForOwner(owner).endpointSource;
613
844
  }
614
845
 
615
846
  export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
616
847
 
617
- export function browserLifecycleState(): BrowserLifecycleState {
618
- if (state.launchPromise) return "starting";
619
- if (state.managedBrowser?.exited) return "exited";
620
- if (state.managedBrowser?.ready) return "running";
621
- if (state.lastLaunchAttempt?.lastError) return "failed";
848
+ 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";
622
853
  return "unobserved";
623
854
  }
624
855
 
625
- export function launchModeLabel() {
626
- if (extensionsConfigured()) {
627
- if (!isLocalDevToolsHost(state.host)) return "invalid; extensions require a local endpoint";
628
- if (!state.autoLaunchEnabled) return "invalid; extensions require auto-launch";
629
- if (state.managedBrowser && !state.managedBrowser.exited) {
630
- return state.portConfigured
856
+ 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)
631
864
  ? "extension-owned browser on explicit port"
632
865
  : "extension-owned browser on dynamic port";
633
866
  }
634
- return state.portConfigured
867
+ return managedBrowserPortConfigured(owner)
635
868
  ? "force extension-owned launch on explicit port"
636
869
  : "force extension-owned launch on dynamic port";
637
870
  }
638
- if (!isLocalDevToolsHost(state.host)) return "manual remote endpoint";
639
- if (!state.autoLaunchEnabled) return "manual; auto-launch disabled";
640
- if (state.managedBrowser && !state.managedBrowser.exited) {
641
- return state.portConfigured
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)
642
875
  ? "auto-launched on explicit port"
643
876
  : "auto-launched on dynamic port";
644
877
  }
645
- return state.portConfigured
878
+ return managedBrowserPortConfigured(owner)
646
879
  ? "attach first; auto-launch explicit port"
647
880
  : "attach first; auto-launch dynamic port";
648
881
  }
649
882
 
650
- export function launchAttemptLines() {
651
- if (!state.lastLaunchAttempt) return [];
883
+ export function launchAttemptLines(owner?: object) {
884
+ const attempt = lastLaunchAttempt(owner);
885
+ if (!attempt) return [];
652
886
 
653
- const lines = [`Last launch attempt: ${state.lastLaunchAttempt.mode}`];
654
- if (state.lastLaunchAttempt.selectedCandidate) {
655
- lines.push(`Launched browser: ${state.lastLaunchAttempt.selectedCandidate}`);
887
+ const lines = [`Last launch attempt: ${attempt.mode}`];
888
+ if (attempt.selectedCandidate) {
889
+ lines.push(`Launched browser: ${attempt.selectedCandidate}`);
656
890
  } else {
657
- lines.push(`Tried browser candidates: ${state.lastLaunchAttempt.candidateLabels.join(", ")}`);
658
- }
659
- if (state.lastLaunchAttempt.userDataDir) {
660
- lines.push(`Managed browser profile: ${state.lastLaunchAttempt.userDataDir}`);
661
- }
662
- if (state.lastLaunchAttempt.lastError) {
663
- lines.push(`Last launch error: ${state.lastLaunchAttempt.lastError}`);
891
+ lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
664
892
  }
893
+ if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
894
+ if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
665
895
  return lines;
666
896
  }
667
897
 
668
- export function launchHint() {
669
- if (extensionsConfigured()) {
898
+ export function launchHint(owner?: object) {
899
+ const browser = browserSettingsForOwner(owner);
900
+ if (extensionsConfigured(owner)) {
670
901
  return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
671
902
  }
672
- if (!isLocalDevToolsHost(state.host)) {
673
- return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint()}.`;
903
+ if (!isLocalDevToolsHost(browser.host)) {
904
+ return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint(owner)}.`;
674
905
  }
675
- if (!state.autoLaunchEnabled) {
676
- return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand()}`;
906
+ if (!browser.autoLaunchEnabled) {
907
+ return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
677
908
  }
678
- const managedMode = state.portConfigured ? `port ${state.port}` : "a dynamic DevTools port";
679
- return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand()}`;
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)}`;
680
913
  }
681
914
 
682
- export function browserCandidateHint() {
683
- if (extensionsConfigured()) {
915
+ export function browserCandidateHint(owner?: object) {
916
+ if (extensionsConfigured(owner)) {
684
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.";
685
918
  }
686
- return `Browser candidates: ${browserCandidateDefinitions()
919
+ return `Browser candidates: ${browserCandidateDefinitions(owner)
687
920
  .map((candidate) => candidate.label)
688
921
  .join(", ")}`;
689
922
  }
690
923
 
691
- export function chromeLaunchCommand() {
692
- const executable = state.browserExecutable ?? defaultManualBrowserExecutable();
924
+ export function chromeLaunchCommand(owner?: object) {
925
+ const executable =
926
+ browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
693
927
  const dataDir =
694
928
  process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
695
- return `${quoteCommandPart(executable)} --remote-debugging-port=${state.port} --user-data-dir=${dataDir}`;
929
+ return `${quoteCommandPart(executable)} --remote-debugging-port=${managedBrowserPort(owner)} --user-data-dir=${dataDir}`;
696
930
  }
697
931
 
698
932
  function defaultManualBrowserExecutable() {
@@ -716,22 +950,23 @@ export function isLocalDevToolsHost(host: string) {
716
950
  return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
717
951
  }
718
952
 
719
- function browserCandidateDefinitions(): BrowserCandidateDefinition[] {
720
- const explicitCandidate = explicitBrowserCandidateDefinition();
953
+ function browserCandidateDefinitions(owner?: object): BrowserCandidateDefinition[] {
954
+ const explicitCandidate = explicitBrowserCandidateDefinition(owner);
721
955
  if (explicitCandidate.length > 0) return explicitCandidate;
722
956
 
723
957
  return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
724
958
  }
725
959
 
726
- function explicitBrowserCandidateDefinition(): BrowserCandidateDefinition[] {
727
- if (!state.browserExecutable) return [];
960
+ function explicitBrowserCandidateDefinition(owner?: object): BrowserCandidateDefinition[] {
961
+ const browser = browserSettingsForOwner(owner);
962
+ if (!browser.executablePath) return [];
728
963
  return [
729
964
  {
730
965
  label:
731
- state.browserExecutableSource === "environment"
966
+ browser.executablePathSource === "environment"
732
967
  ? "PI_CHROME_DEVTOOLS_BROWSER"
733
968
  : "browser.executablePath",
734
- executable: state.browserExecutable,
969
+ executable: browser.executablePath,
735
970
  source: "env",
736
971
  },
737
972
  ];
@@ -828,11 +1063,12 @@ async function resolveBrowserCandidates(
828
1063
  definitions: BrowserCandidateDefinition[],
829
1064
  generation: number,
830
1065
  signal: AbortSignal,
1066
+ owner?: object,
831
1067
  ) {
832
1068
  const candidates: BrowserCandidate[] = [];
833
1069
  for (const definition of definitions) {
834
1070
  const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
835
- throwIfBrowserLaunchCancelled(generation, signal);
1071
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
836
1072
  if (!resolvedExecutable) continue;
837
1073
  candidates.push({ ...definition, resolvedExecutable });
838
1074
  }
@@ -897,8 +1133,11 @@ function formatBrowserCandidateDefinition(candidate: BrowserCandidateDefinition)
897
1133
  return `${candidate.label} (${candidate.executable})`;
898
1134
  }
899
1135
 
900
- function noBrowserCandidateMessage(candidateDefinitions: BrowserCandidateDefinition[]) {
901
- if (extensionsConfigured()) {
1136
+ function noBrowserCandidateMessage(
1137
+ candidateDefinitions: BrowserCandidateDefinition[],
1138
+ owner?: object,
1139
+ ) {
1140
+ if (extensionsConfigured(owner)) {
902
1141
  return [
903
1142
  "Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
904
1143
  `Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,