@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.
- package/README.md +62 -9
- package/dist/chunks/{browser-settings-menu-VWCNG4HB.js → browser-settings-menu-REIU7C3Q.js} +83 -7
- package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +7 -0
- package/dist/chunks/chunk-GTTYS5GT.js +384 -0
- package/dist/chunks/chunk-GTTYS5GT.js.map +7 -0
- package/dist/chunks/{chunk-RH6KESL3.js → chunk-SRT3UUIP.js} +473 -574
- package/dist/chunks/chunk-SRT3UUIP.js.map +7 -0
- package/dist/chunks/chunk-YJVYJ2L2.js +469 -0
- package/dist/chunks/chunk-YJVYJ2L2.js.map +7 -0
- package/dist/chunks/{menu-PLD3S3EO.js → menu-H5HMVGHI.js} +72 -39
- package/dist/chunks/menu-H5HMVGHI.js.map +7 -0
- package/dist/chunks/tools-6P7EUX37.js +964 -0
- package/dist/chunks/tools-6P7EUX37.js.map +7 -0
- package/dist/index.ts +166 -199
- package/dist/index.ts.map +4 -4
- package/package.json +4 -3
- package/src/browser-manager.ts +408 -169
- package/src/browser-settings-menu.ts +100 -5
- package/src/cdp-client.ts +347 -90
- package/src/chrome-devtools.ts +58 -11
- package/src/lazy-tools.ts +37 -4
- package/src/menu.ts +80 -39
- package/src/render.ts +12 -1
- package/src/runtime.ts +92 -0
- package/src/settings.ts +47 -10
- package/src/tool-names.ts +16 -1
- package/src/tool-selector.ts +85 -44
- package/src/tools.ts +91 -15
- package/src/webmcp/discovery.ts +235 -0
- package/src/webmcp/policy.ts +409 -0
- package/src/webmcp/protocol.ts +426 -0
- package/src/webmcp/tools.ts +190 -0
- package/dist/chunks/browser-settings-menu-VWCNG4HB.js.map +0 -7
- package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
- package/dist/chunks/menu-PLD3S3EO.js.map +0 -7
package/src/browser-manager.ts
CHANGED
|
@@ -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(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 (
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
307
|
+
const existingBrowser = managedBrowserForOwner(owner);
|
|
308
|
+
if (existingBrowser) {
|
|
309
|
+
await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
|
|
310
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
140
311
|
}
|
|
141
|
-
if (
|
|
142
|
-
const available = await browserManagerOperations.isPortAvailable(
|
|
143
|
-
|
|
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 ${
|
|
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(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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
|
-
|
|
176
|
-
...
|
|
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:
|
|
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
|
-
|
|
186
|
-
...
|
|
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
|
-
|
|
193
|
-
...
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
287
|
-
|
|
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 (
|
|
294
|
-
launchedBrowser.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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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 ===
|
|
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
|
|
444
|
-
options: { awaitLaunch?: boolean; cancelLaunch?: boolean } = {},
|
|
649
|
+
managedBrowser?: ManagedBrowser,
|
|
650
|
+
options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
|
|
445
651
|
) {
|
|
446
|
-
|
|
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
|
|
449
|
-
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 (
|
|
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 (!
|
|
474
|
-
|
|
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>(
|
|
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(
|
|
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 !
|
|
808
|
+
return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
|
|
579
809
|
}
|
|
580
810
|
|
|
581
|
-
function canAutoLaunchBrowser() {
|
|
582
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
619
|
-
if (
|
|
620
|
-
if (
|
|
621
|
-
if (
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
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
|
|
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(
|
|
639
|
-
if (!
|
|
640
|
-
if (
|
|
641
|
-
return
|
|
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
|
|
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
|
-
|
|
883
|
+
export function launchAttemptLines(owner?: object) {
|
|
884
|
+
const attempt = lastLaunchAttempt(owner);
|
|
885
|
+
if (!attempt) return [];
|
|
652
886
|
|
|
653
|
-
const lines = [`Last launch attempt: ${
|
|
654
|
-
if (
|
|
655
|
-
lines.push(`Launched browser: ${
|
|
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: ${
|
|
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
|
-
|
|
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(
|
|
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 (!
|
|
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 =
|
|
679
|
-
|
|
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 =
|
|
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=${
|
|
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
|
-
|
|
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
|
-
|
|
966
|
+
browser.executablePathSource === "environment"
|
|
732
967
|
? "PI_CHROME_DEVTOOLS_BROWSER"
|
|
733
968
|
: "browser.executablePath",
|
|
734
|
-
executable:
|
|
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(
|
|
901
|
-
|
|
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(", ")}`,
|