@narumitw/pi-chrome-devtools 0.52.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -17
- package/dist/chunks/{browser-settings-menu-VI2I3TXI.js → browser-settings-menu-N5O65I7S.js} +81 -7
- package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
- package/dist/chunks/chunk-B7B63PWB.js +465 -0
- package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
- package/dist/chunks/{chunk-CC37WUUE.js → chunk-GVBXT3FB.js} +369 -461
- package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
- package/dist/chunks/chunk-RTECHAEZ.js +373 -0
- package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
- package/dist/chunks/{menu-LLR455UX.js → menu-UBHW6UP2.js} +75 -41
- package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
- package/dist/chunks/tools-V7OR5KJ3.js +945 -0
- package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
- package/dist/index.ts +166 -196
- package/dist/index.ts.map +4 -4
- package/package.json +4 -3
- package/src/browser-manager.ts +282 -109
- package/src/browser-settings-menu.ts +98 -4
- package/src/cdp-client.ts +329 -83
- package/src/chrome-devtools.ts +61 -13
- package/src/lazy-tools.ts +105 -8
- package/src/menu.ts +83 -41
- 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 +79 -36
- package/src/tools.ts +86 -10
- package/src/webmcp/discovery.ts +235 -0
- package/src/webmcp/policy.ts +402 -0
- package/src/webmcp/protocol.ts +414 -0
- package/src/webmcp/tools.ts +190 -0
- package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +0 -7
- package/dist/chunks/chunk-CC37WUUE.js.map +0 -7
- package/dist/chunks/menu-LLR455UX.js.map +0 -7
package/src/browser-manager.ts
CHANGED
|
@@ -8,16 +8,20 @@ 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";
|
|
20
22
|
|
|
23
|
+
// Endpoint attachment, managed launch, dynamic-port publication, and shutdown intentionally remain
|
|
24
|
+
// together because they form one ownership state machine behind the testable operations boundary.
|
|
21
25
|
interface BrowserSpawnOptions {
|
|
22
26
|
shell: false;
|
|
23
27
|
stdio: "ignore";
|
|
@@ -49,6 +53,80 @@ const DEFAULT_BROWSER_MANAGER_OPERATIONS: BrowserManagerOperations = {
|
|
|
49
53
|
|
|
50
54
|
let browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
|
|
51
55
|
|
|
56
|
+
interface ManagedBrowserScope {
|
|
57
|
+
controller: AbortController;
|
|
58
|
+
generation: number;
|
|
59
|
+
launchPromise?: Promise<void>;
|
|
60
|
+
managedBrowser?: ManagedBrowser;
|
|
61
|
+
port: number;
|
|
62
|
+
lastLaunchAttempt?: BrowserLaunchAttempt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const managedBrowserScopes = new WeakMap<object, ManagedBrowserScope>();
|
|
66
|
+
|
|
67
|
+
function managedBrowserScope(owner: object) {
|
|
68
|
+
const existing = managedBrowserScopes.get(owner);
|
|
69
|
+
if (existing) return existing;
|
|
70
|
+
const created: ManagedBrowserScope = {
|
|
71
|
+
controller: new AbortController(),
|
|
72
|
+
generation: 0,
|
|
73
|
+
port: state.configuredPort,
|
|
74
|
+
};
|
|
75
|
+
managedBrowserScopes.set(owner, created);
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function startManagedBrowserSession(owner: object) {
|
|
80
|
+
const scope = managedBrowserScope(owner);
|
|
81
|
+
scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
|
|
82
|
+
scope.controller = new AbortController();
|
|
83
|
+
scope.generation += 1;
|
|
84
|
+
scope.port = state.configuredPort;
|
|
85
|
+
scope.lastLaunchAttempt = undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function managedBrowserForOwner(owner?: object) {
|
|
89
|
+
if (!owner) return state.managedBrowser;
|
|
90
|
+
const scopedBrowser = managedBrowserScope(owner).managedBrowser;
|
|
91
|
+
if (scopedBrowser) return scopedBrowser;
|
|
92
|
+
return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function setManagedBrowser(owner: object | undefined, browser: ManagedBrowser | undefined) {
|
|
96
|
+
if (owner) managedBrowserScope(owner).managedBrowser = browser;
|
|
97
|
+
else state.managedBrowser = browser;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function launchPromiseForOwner(owner?: object) {
|
|
101
|
+
return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setLaunchPromise(owner: object | undefined, launchPromise: Promise<void> | undefined) {
|
|
105
|
+
if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
|
|
106
|
+
else state.launchPromise = launchPromise;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function managedBrowserPort(owner?: object) {
|
|
110
|
+
return owner ? managedBrowserScope(owner).port : state.port;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function setManagedBrowserPort(owner: object | undefined, port: number) {
|
|
114
|
+
if (owner) managedBrowserScope(owner).port = port;
|
|
115
|
+
else state.port = port;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function lastLaunchAttempt(owner?: object) {
|
|
119
|
+
return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setLastLaunchAttempt(
|
|
123
|
+
owner: object | undefined,
|
|
124
|
+
attempt: BrowserLaunchAttempt | undefined,
|
|
125
|
+
) {
|
|
126
|
+
if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
|
|
127
|
+
else state.lastLaunchAttempt = attempt;
|
|
128
|
+
}
|
|
129
|
+
|
|
52
130
|
export function setBrowserManagerOperationsForTests(overrides: Partial<BrowserManagerOperations>) {
|
|
53
131
|
const previous = browserManagerOperations;
|
|
54
132
|
browserManagerOperations = { ...DEFAULT_BROWSER_MANAGER_OPERATIONS, ...overrides };
|
|
@@ -69,19 +147,28 @@ function normalizePathForComparison(value: string) {
|
|
|
69
147
|
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
70
148
|
}
|
|
71
149
|
|
|
72
|
-
export async function ensureDevToolsEndpoint(
|
|
150
|
+
export async function ensureDevToolsEndpoint(
|
|
151
|
+
waitMs = DEFAULT_ENDPOINT_WAIT_MS,
|
|
152
|
+
callerSignal?: AbortSignal,
|
|
153
|
+
owner?: object,
|
|
154
|
+
) {
|
|
155
|
+
callerSignal?.throwIfAborted();
|
|
73
156
|
if (extensionsConfigured()) {
|
|
74
157
|
validateExtensionLaunchMode();
|
|
75
|
-
await ensureManagedBrowserLaunched(waitMs);
|
|
158
|
+
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
76
159
|
return;
|
|
77
160
|
}
|
|
78
161
|
if (canAutoLaunchBrowser()) {
|
|
79
162
|
try {
|
|
80
|
-
await withEndpointRetry(
|
|
163
|
+
await withEndpointRetry(
|
|
164
|
+
() => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
|
|
165
|
+
waitMs,
|
|
166
|
+
callerSignal,
|
|
167
|
+
);
|
|
81
168
|
return;
|
|
82
169
|
} catch (error) {
|
|
83
170
|
if (shouldAutoLaunchAfterEndpointError(error)) {
|
|
84
|
-
await ensureManagedBrowserLaunched(waitMs);
|
|
171
|
+
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
85
172
|
return;
|
|
86
173
|
}
|
|
87
174
|
throw error;
|
|
@@ -89,7 +176,7 @@ export async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS)
|
|
|
89
176
|
}
|
|
90
177
|
|
|
91
178
|
try {
|
|
92
|
-
await fetchDevToolsJson<unknown>("/json/version");
|
|
179
|
+
await fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner);
|
|
93
180
|
} catch (error) {
|
|
94
181
|
if (isRetryableEndpointError(error)) return;
|
|
95
182
|
throw error;
|
|
@@ -114,51 +201,97 @@ function validateExtensionLaunchMode() {
|
|
|
114
201
|
}
|
|
115
202
|
}
|
|
116
203
|
|
|
117
|
-
async function ensureManagedBrowserLaunched(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
204
|
+
async function ensureManagedBrowserLaunched(
|
|
205
|
+
waitMs: number,
|
|
206
|
+
callerSignal?: AbortSignal,
|
|
207
|
+
owner?: object,
|
|
208
|
+
) {
|
|
209
|
+
const existingLaunch = launchPromiseForOwner(owner);
|
|
210
|
+
if (existingLaunch) {
|
|
211
|
+
await waitForCaller(existingLaunch, callerSignal);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const existingBrowser = managedBrowserForOwner(owner);
|
|
215
|
+
if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
|
|
216
|
+
const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
|
|
217
|
+
const signal = owner
|
|
218
|
+
? managedBrowserScope(owner).controller.signal
|
|
219
|
+
: state.sessionController.signal;
|
|
220
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
221
|
+
|
|
222
|
+
const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
|
|
125
223
|
const wrappedPromise = launchPromise.finally(() => {
|
|
126
|
-
if (
|
|
224
|
+
if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, undefined);
|
|
225
|
+
});
|
|
226
|
+
setLaunchPromise(owner, wrappedPromise);
|
|
227
|
+
await waitForCaller(wrappedPromise, callerSignal);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function waitForCaller<T>(operation: Promise<T>, signal?: AbortSignal) {
|
|
231
|
+
if (!signal) return operation;
|
|
232
|
+
signal.throwIfAborted();
|
|
233
|
+
return new Promise<T>((resolve, reject) => {
|
|
234
|
+
let settled = false;
|
|
235
|
+
const onAbort = () => settle(() => reject(signal.reason));
|
|
236
|
+
const settle = (callback: () => void) => {
|
|
237
|
+
if (settled) return;
|
|
238
|
+
settled = true;
|
|
239
|
+
signal.removeEventListener("abort", onAbort);
|
|
240
|
+
callback();
|
|
241
|
+
};
|
|
242
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
243
|
+
operation.then(
|
|
244
|
+
(value) => settle(() => resolve(value)),
|
|
245
|
+
(error) => settle(() => reject(error)),
|
|
246
|
+
);
|
|
127
247
|
});
|
|
128
|
-
state.launchPromise = wrappedPromise;
|
|
129
|
-
return wrappedPromise;
|
|
130
248
|
}
|
|
131
249
|
|
|
132
250
|
async function prepareManagedBrowserLaunch(
|
|
133
251
|
waitMs: number,
|
|
134
252
|
generation: number,
|
|
135
253
|
signal: AbortSignal,
|
|
254
|
+
owner?: object,
|
|
136
255
|
) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
256
|
+
const existingBrowser = managedBrowserForOwner(owner);
|
|
257
|
+
if (existingBrowser) {
|
|
258
|
+
await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
|
|
259
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
140
260
|
}
|
|
141
261
|
if (state.portConfigured) {
|
|
142
|
-
const available = await browserManagerOperations.isPortAvailable(
|
|
143
|
-
|
|
262
|
+
const available = await browserManagerOperations.isPortAvailable(
|
|
263
|
+
state.host,
|
|
264
|
+
managedBrowserPort(owner),
|
|
265
|
+
);
|
|
266
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
144
267
|
if (!available) {
|
|
145
268
|
throw new DevToolsEndpointError(
|
|
146
|
-
`Cannot launch the extension-owned browser because explicit port ${
|
|
269
|
+
`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
270
|
);
|
|
148
271
|
}
|
|
149
272
|
}
|
|
150
|
-
return launchManagedBrowser(waitMs, generation, signal);
|
|
273
|
+
return launchManagedBrowser(waitMs, generation, signal, owner);
|
|
151
274
|
}
|
|
152
275
|
|
|
153
|
-
async function launchManagedBrowser(
|
|
154
|
-
|
|
276
|
+
async function launchManagedBrowser(
|
|
277
|
+
waitMs: number,
|
|
278
|
+
generation: number,
|
|
279
|
+
signal: AbortSignal,
|
|
280
|
+
owner?: object,
|
|
281
|
+
) {
|
|
282
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
155
283
|
const candidateDefinitions = browserCandidateDefinitions();
|
|
156
|
-
const candidates = await resolveBrowserCandidates(
|
|
157
|
-
|
|
158
|
-
|
|
284
|
+
const candidates = await resolveBrowserCandidates(
|
|
285
|
+
candidateDefinitions,
|
|
286
|
+
generation,
|
|
287
|
+
signal,
|
|
288
|
+
owner,
|
|
289
|
+
);
|
|
290
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
291
|
+
setLastLaunchAttempt(owner, {
|
|
159
292
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
160
293
|
mode: state.portConfigured ? "explicit-port" : "dynamic-port",
|
|
161
|
-
};
|
|
294
|
+
});
|
|
162
295
|
|
|
163
296
|
if (candidates.length === 0) {
|
|
164
297
|
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
|
|
@@ -166,33 +299,41 @@ async function launchManagedBrowser(waitMs: number, generation: number, signal:
|
|
|
166
299
|
|
|
167
300
|
let lastError: unknown;
|
|
168
301
|
for (const candidate of candidates) {
|
|
169
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
302
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
170
303
|
try {
|
|
171
304
|
if (extensionsConfigured())
|
|
172
|
-
await requireSupportedExtensionBrowser(candidate, generation, signal);
|
|
173
|
-
await launchBrowserCandidate(candidate, waitMs, generation, signal);
|
|
174
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
175
|
-
|
|
176
|
-
...
|
|
305
|
+
await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
|
|
306
|
+
await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
|
|
307
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
308
|
+
setLastLaunchAttempt(owner, {
|
|
309
|
+
...lastLaunchAttempt(owner),
|
|
310
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
311
|
+
mode: state.portConfigured ? "explicit-port" : "dynamic-port",
|
|
177
312
|
selectedCandidate: formatBrowserCandidate(candidate),
|
|
178
|
-
userDataDir:
|
|
179
|
-
};
|
|
313
|
+
userDataDir: managedBrowserForOwner(owner)?.userDataDir,
|
|
314
|
+
});
|
|
180
315
|
return;
|
|
181
316
|
} catch (error) {
|
|
182
317
|
try {
|
|
183
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
318
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
184
319
|
} catch (cancellation) {
|
|
185
|
-
|
|
186
|
-
...
|
|
320
|
+
setLastLaunchAttempt(owner, {
|
|
321
|
+
...(lastLaunchAttempt(owner) ?? {
|
|
322
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
323
|
+
mode: state.portConfigured ? "explicit-port" : "dynamic-port",
|
|
324
|
+
}),
|
|
187
325
|
lastError: formatError(cancellation),
|
|
188
|
-
};
|
|
326
|
+
});
|
|
189
327
|
throw cancellation;
|
|
190
328
|
}
|
|
191
329
|
lastError = error;
|
|
192
|
-
|
|
193
|
-
...
|
|
330
|
+
setLastLaunchAttempt(owner, {
|
|
331
|
+
...(lastLaunchAttempt(owner) ?? {
|
|
332
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
333
|
+
mode: state.portConfigured ? "explicit-port" : "dynamic-port",
|
|
334
|
+
}),
|
|
194
335
|
lastError: formatError(error),
|
|
195
|
-
};
|
|
336
|
+
});
|
|
196
337
|
}
|
|
197
338
|
}
|
|
198
339
|
|
|
@@ -214,6 +355,7 @@ async function requireSupportedExtensionBrowser(
|
|
|
214
355
|
candidate: BrowserCandidate,
|
|
215
356
|
generation: number,
|
|
216
357
|
signal: AbortSignal,
|
|
358
|
+
owner?: object,
|
|
217
359
|
) {
|
|
218
360
|
let version: string;
|
|
219
361
|
try {
|
|
@@ -221,12 +363,12 @@ async function requireSupportedExtensionBrowser(
|
|
|
221
363
|
await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)
|
|
222
364
|
).trim();
|
|
223
365
|
} catch (error) {
|
|
224
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
366
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
225
367
|
throw new DevToolsEndpointError(
|
|
226
368
|
`Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError(error)}. Configure Chrome for Testing or Chromium.`,
|
|
227
369
|
);
|
|
228
370
|
}
|
|
229
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
371
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
230
372
|
const classification = classifyExtensionBrowserVersion(version);
|
|
231
373
|
if (!classification.supported) {
|
|
232
374
|
throw new DevToolsEndpointError(
|
|
@@ -252,15 +394,16 @@ async function launchBrowserCandidate(
|
|
|
252
394
|
waitMs: number,
|
|
253
395
|
generation: number,
|
|
254
396
|
signal: AbortSignal,
|
|
397
|
+
owner?: object,
|
|
255
398
|
) {
|
|
256
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
399
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
257
400
|
const userDataDir = await browserManagerOperations.mkdtemp(
|
|
258
401
|
join(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX),
|
|
259
402
|
);
|
|
260
403
|
let managedBrowser: ManagedBrowser | undefined;
|
|
261
404
|
try {
|
|
262
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
263
|
-
const portArgument = state.portConfigured ? String(
|
|
405
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
406
|
+
const portArgument = state.portConfigured ? String(managedBrowserPort(owner)) : "0";
|
|
264
407
|
const args = buildManagedBrowserLaunchArguments(
|
|
265
408
|
userDataDir,
|
|
266
409
|
portArgument,
|
|
@@ -276,22 +419,26 @@ async function launchBrowserCandidate(
|
|
|
276
419
|
exited: false,
|
|
277
420
|
ready: false,
|
|
278
421
|
ownerGeneration: generation,
|
|
422
|
+
...(owner ? { sessionOwner: owner } : {}),
|
|
279
423
|
};
|
|
280
424
|
managedBrowser = launchedBrowser;
|
|
281
|
-
|
|
425
|
+
setManagedBrowser(owner, launchedBrowser);
|
|
282
426
|
|
|
283
427
|
child.once("exit", () => {
|
|
428
|
+
if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
|
|
429
|
+
invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
|
|
430
|
+
}
|
|
284
431
|
launchedBrowser.exited = true;
|
|
285
432
|
launchedBrowser.ready = false;
|
|
286
|
-
if (!state.portConfigured && launchedBrowser.port ===
|
|
287
|
-
|
|
433
|
+
if (!state.portConfigured && launchedBrowser.port === managedBrowserPort(owner)) {
|
|
434
|
+
setManagedBrowserPort(owner, state.configuredPort);
|
|
288
435
|
}
|
|
289
436
|
});
|
|
290
437
|
|
|
291
438
|
await waitForBrowserSpawn(child);
|
|
292
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
439
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
293
440
|
if (state.portConfigured) {
|
|
294
|
-
launchedBrowser.port =
|
|
441
|
+
launchedBrowser.port = managedBrowserPort(owner);
|
|
295
442
|
} else {
|
|
296
443
|
launchedBrowser.port = await readManagedBrowserPort(
|
|
297
444
|
userDataDir,
|
|
@@ -299,15 +446,16 @@ async function launchBrowserCandidate(
|
|
|
299
446
|
waitMs,
|
|
300
447
|
generation,
|
|
301
448
|
signal,
|
|
449
|
+
owner,
|
|
302
450
|
);
|
|
303
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
304
|
-
|
|
451
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
452
|
+
setManagedBrowserPort(owner, launchedBrowser.port);
|
|
305
453
|
}
|
|
306
|
-
await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
|
|
307
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
454
|
+
await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
|
|
455
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
308
456
|
launchedBrowser.ready = true;
|
|
309
457
|
} catch (error) {
|
|
310
|
-
if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
|
|
458
|
+
if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
|
|
311
459
|
else {
|
|
312
460
|
await browserManagerOperations
|
|
313
461
|
.rm(userDataDir, { recursive: true, force: true })
|
|
@@ -362,6 +510,7 @@ async function readManagedBrowserPort(
|
|
|
362
510
|
waitMs: number,
|
|
363
511
|
generation: number,
|
|
364
512
|
signal: AbortSignal,
|
|
513
|
+
owner?: object,
|
|
365
514
|
) {
|
|
366
515
|
const activePortFile = join(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
|
|
367
516
|
const deadline = Date.now() + waitMs;
|
|
@@ -373,7 +522,7 @@ async function readManagedBrowserPort(
|
|
|
373
522
|
if (isNodeError(error) && error.code === "ENOENT") return undefined;
|
|
374
523
|
throw error;
|
|
375
524
|
});
|
|
376
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
525
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
377
526
|
const portText = text?.split(/\r?\n/, 1)[0]?.trim();
|
|
378
527
|
const port = Number(portText);
|
|
379
528
|
if (Number.isInteger(port) && port > 0) return port;
|
|
@@ -397,24 +546,25 @@ async function waitForDevToolsEndpoint(
|
|
|
397
546
|
managedBrowser: ManagedBrowser,
|
|
398
547
|
generation: number,
|
|
399
548
|
signal: AbortSignal,
|
|
549
|
+
owner?: object,
|
|
400
550
|
) {
|
|
401
551
|
const deadline = Date.now() + waitMs;
|
|
402
552
|
while (true) {
|
|
403
553
|
throwIfManagedBrowserExited(managedBrowser);
|
|
404
554
|
try {
|
|
405
|
-
await fetchDevToolsJson<unknown>("/json/version", { signal });
|
|
406
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
555
|
+
await fetchDevToolsJson<unknown>("/json/version", { signal }, owner);
|
|
556
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
407
557
|
return;
|
|
408
558
|
} catch (error) {
|
|
409
559
|
if (!isRetryableEndpointError(error)) throw error;
|
|
410
560
|
}
|
|
411
561
|
|
|
412
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
562
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
413
563
|
const remainingMs = deadline - Date.now();
|
|
414
564
|
if (remainingMs <= 0) {
|
|
415
565
|
throw new DevToolsEndpointError(
|
|
416
566
|
[
|
|
417
|
-
`Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
|
|
567
|
+
`Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
|
|
418
568
|
launchHint(),
|
|
419
569
|
].join("\n"),
|
|
420
570
|
);
|
|
@@ -428,36 +578,52 @@ function throwIfManagedBrowserExited(managedBrowser: ManagedBrowser) {
|
|
|
428
578
|
throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
|
|
429
579
|
}
|
|
430
580
|
|
|
431
|
-
function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
581
|
+
function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal, owner?: object) {
|
|
582
|
+
const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
|
|
583
|
+
const shuttingDown = owner ? signal.aborted : state.shuttingDown;
|
|
584
|
+
if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
|
|
435
585
|
throw new DevToolsEndpointError(
|
|
436
|
-
generation ===
|
|
586
|
+
generation === currentGeneration
|
|
437
587
|
? "Chrome DevTools browser launch cancelled during shutdown."
|
|
438
588
|
: "Chrome DevTools browser launch cancelled because the session was replaced.",
|
|
439
589
|
);
|
|
440
590
|
}
|
|
441
591
|
|
|
442
592
|
export async function shutdownManagedBrowser(
|
|
443
|
-
managedBrowser
|
|
444
|
-
options: { awaitLaunch?: boolean; cancelLaunch?: boolean } = {},
|
|
593
|
+
managedBrowser?: ManagedBrowser,
|
|
594
|
+
options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
|
|
445
595
|
) {
|
|
446
|
-
|
|
596
|
+
const owner = options.owner ?? managedBrowser?.sessionOwner;
|
|
597
|
+
if (options.cancelLaunch) {
|
|
598
|
+
if (owner) {
|
|
599
|
+
const scope = managedBrowserScope(owner);
|
|
600
|
+
scope.controller.abort(
|
|
601
|
+
new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError"),
|
|
602
|
+
);
|
|
603
|
+
scope.generation += 1;
|
|
604
|
+
} else {
|
|
605
|
+
state.shuttingDown = true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
managedBrowser ??= managedBrowserForOwner(owner);
|
|
447
609
|
if (options.awaitLaunch !== false) {
|
|
448
|
-
await
|
|
449
|
-
managedBrowser
|
|
610
|
+
await launchPromiseForOwner(owner)?.catch(() => undefined);
|
|
611
|
+
managedBrowser ??= managedBrowserForOwner(owner);
|
|
450
612
|
}
|
|
451
613
|
if (!managedBrowser) return;
|
|
452
614
|
if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
|
|
453
615
|
|
|
454
|
-
const cleanup = cleanupManagedBrowser(managedBrowser);
|
|
616
|
+
const cleanup = cleanupManagedBrowser(managedBrowser, owner);
|
|
455
617
|
managedBrowser.cleanupPromise = cleanup;
|
|
456
618
|
return cleanup;
|
|
457
619
|
}
|
|
458
620
|
|
|
459
|
-
async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
|
|
460
|
-
if (
|
|
621
|
+
async function cleanupManagedBrowser(managedBrowser: ManagedBrowser, owner?: object) {
|
|
622
|
+
if (managedBrowserForOwner(owner) === managedBrowser) {
|
|
623
|
+
if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
|
|
624
|
+
if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
|
|
625
|
+
else setManagedBrowser(owner, undefined);
|
|
626
|
+
}
|
|
461
627
|
if (!managedBrowser.exited) {
|
|
462
628
|
killManagedBrowserProcess(managedBrowser);
|
|
463
629
|
await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
|
|
@@ -470,8 +636,8 @@ async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
|
|
|
470
636
|
await browserManagerOperations
|
|
471
637
|
.rm(managedBrowser.userDataDir, { recursive: true, force: true })
|
|
472
638
|
.catch(() => undefined);
|
|
473
|
-
if (!state.portConfigured && managedBrowser.port ===
|
|
474
|
-
|
|
639
|
+
if (!state.portConfigured && managedBrowser.port === managedBrowserPort(owner)) {
|
|
640
|
+
setManagedBrowserPort(owner, state.configuredPort);
|
|
475
641
|
}
|
|
476
642
|
}
|
|
477
643
|
|
|
@@ -505,8 +671,8 @@ function waitForManagedBrowserExit(managedBrowser: ManagedBrowser, waitMs: numbe
|
|
|
505
671
|
});
|
|
506
672
|
}
|
|
507
673
|
|
|
508
|
-
export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
|
|
509
|
-
const url = `${devToolsEndpoint()}${path}`;
|
|
674
|
+
export async function fetchDevToolsJson<T>(path: string, init?: RequestInit, owner?: object) {
|
|
675
|
+
const url = `${devToolsEndpoint(owner)}${path}`;
|
|
510
676
|
let response: Response;
|
|
511
677
|
try {
|
|
512
678
|
const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
|
|
@@ -515,7 +681,7 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
|
|
|
515
681
|
signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal,
|
|
516
682
|
});
|
|
517
683
|
} catch (error) {
|
|
518
|
-
throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
|
|
684
|
+
throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
|
|
519
685
|
launchable: true,
|
|
520
686
|
retryable: true,
|
|
521
687
|
});
|
|
@@ -545,9 +711,14 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
|
|
|
545
711
|
}
|
|
546
712
|
}
|
|
547
713
|
|
|
548
|
-
export async function withEndpointRetry<T>(
|
|
714
|
+
export async function withEndpointRetry<T>(
|
|
715
|
+
operation: () => Promise<T>,
|
|
716
|
+
waitMs: number,
|
|
717
|
+
signal?: AbortSignal,
|
|
718
|
+
) {
|
|
549
719
|
const deadline = Date.now() + waitMs;
|
|
550
720
|
while (true) {
|
|
721
|
+
signal?.throwIfAborted();
|
|
551
722
|
try {
|
|
552
723
|
return await operation();
|
|
553
724
|
} catch (error) {
|
|
@@ -556,7 +727,10 @@ export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs:
|
|
|
556
727
|
const remainingMs = deadline - Date.now();
|
|
557
728
|
if (remainingMs <= 0) throw error;
|
|
558
729
|
|
|
559
|
-
await browserManagerOperations.sleep(
|
|
730
|
+
await browserManagerOperations.sleep(
|
|
731
|
+
Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
|
|
732
|
+
signal,
|
|
733
|
+
);
|
|
560
734
|
}
|
|
561
735
|
}
|
|
562
736
|
}
|
|
@@ -586,10 +760,10 @@ function extensionsConfigured() {
|
|
|
586
760
|
return state.extensionPaths.length > 0;
|
|
587
761
|
}
|
|
588
762
|
|
|
589
|
-
function endpointConnectionErrorMessage(error: unknown) {
|
|
763
|
+
function endpointConnectionErrorMessage(error: unknown, owner?: object) {
|
|
590
764
|
const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
|
|
591
765
|
return [
|
|
592
|
-
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
|
|
766
|
+
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
|
|
593
767
|
launchHint(),
|
|
594
768
|
endpointConfigHint(),
|
|
595
769
|
].join("\n");
|
|
@@ -599,8 +773,8 @@ function isTimeoutError(error: unknown) {
|
|
|
599
773
|
return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
600
774
|
}
|
|
601
775
|
|
|
602
|
-
export function devToolsEndpoint() {
|
|
603
|
-
return `http://${formatHostForUrl(state.host)}:${
|
|
776
|
+
export function devToolsEndpoint(owner?: object) {
|
|
777
|
+
return `http://${formatHostForUrl(state.host)}:${managedBrowserPort(owner)}`;
|
|
604
778
|
}
|
|
605
779
|
|
|
606
780
|
export function formatHostForUrl(host: string) {
|
|
@@ -614,19 +788,20 @@ export function endpointSourceLabel() {
|
|
|
614
788
|
|
|
615
789
|
export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
|
|
616
790
|
|
|
617
|
-
export function browserLifecycleState(): BrowserLifecycleState {
|
|
618
|
-
if (
|
|
619
|
-
if (
|
|
620
|
-
if (
|
|
621
|
-
if (
|
|
791
|
+
export function browserLifecycleState(owner?: object): BrowserLifecycleState {
|
|
792
|
+
if (launchPromiseForOwner(owner)) return "starting";
|
|
793
|
+
if (managedBrowserForOwner(owner)?.exited) return "exited";
|
|
794
|
+
if (managedBrowserForOwner(owner)?.ready) return "running";
|
|
795
|
+
if (lastLaunchAttempt(owner)?.lastError) return "failed";
|
|
622
796
|
return "unobserved";
|
|
623
797
|
}
|
|
624
798
|
|
|
625
|
-
export function launchModeLabel() {
|
|
799
|
+
export function launchModeLabel(owner?: object) {
|
|
800
|
+
const managedBrowser = managedBrowserForOwner(owner);
|
|
626
801
|
if (extensionsConfigured()) {
|
|
627
802
|
if (!isLocalDevToolsHost(state.host)) return "invalid; extensions require a local endpoint";
|
|
628
803
|
if (!state.autoLaunchEnabled) return "invalid; extensions require auto-launch";
|
|
629
|
-
if (
|
|
804
|
+
if (managedBrowser && !managedBrowser.exited) {
|
|
630
805
|
return state.portConfigured
|
|
631
806
|
? "extension-owned browser on explicit port"
|
|
632
807
|
: "extension-owned browser on dynamic port";
|
|
@@ -637,7 +812,7 @@ export function launchModeLabel() {
|
|
|
637
812
|
}
|
|
638
813
|
if (!isLocalDevToolsHost(state.host)) return "manual remote endpoint";
|
|
639
814
|
if (!state.autoLaunchEnabled) return "manual; auto-launch disabled";
|
|
640
|
-
if (
|
|
815
|
+
if (managedBrowser && !managedBrowser.exited) {
|
|
641
816
|
return state.portConfigured
|
|
642
817
|
? "auto-launched on explicit port"
|
|
643
818
|
: "auto-launched on dynamic port";
|
|
@@ -647,21 +822,18 @@ export function launchModeLabel() {
|
|
|
647
822
|
: "attach first; auto-launch dynamic port";
|
|
648
823
|
}
|
|
649
824
|
|
|
650
|
-
export function launchAttemptLines() {
|
|
651
|
-
|
|
825
|
+
export function launchAttemptLines(owner?: object) {
|
|
826
|
+
const attempt = lastLaunchAttempt(owner);
|
|
827
|
+
if (!attempt) return [];
|
|
652
828
|
|
|
653
|
-
const lines = [`Last launch attempt: ${
|
|
654
|
-
if (
|
|
655
|
-
lines.push(`Launched browser: ${
|
|
829
|
+
const lines = [`Last launch attempt: ${attempt.mode}`];
|
|
830
|
+
if (attempt.selectedCandidate) {
|
|
831
|
+
lines.push(`Launched browser: ${attempt.selectedCandidate}`);
|
|
656
832
|
} 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}`);
|
|
833
|
+
lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
|
|
664
834
|
}
|
|
835
|
+
if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
|
|
836
|
+
if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
|
|
665
837
|
return lines;
|
|
666
838
|
}
|
|
667
839
|
|
|
@@ -828,11 +1000,12 @@ async function resolveBrowserCandidates(
|
|
|
828
1000
|
definitions: BrowserCandidateDefinition[],
|
|
829
1001
|
generation: number,
|
|
830
1002
|
signal: AbortSignal,
|
|
1003
|
+
owner?: object,
|
|
831
1004
|
) {
|
|
832
1005
|
const candidates: BrowserCandidate[] = [];
|
|
833
1006
|
for (const definition of definitions) {
|
|
834
1007
|
const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
|
|
835
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
1008
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
836
1009
|
if (!resolvedExecutable) continue;
|
|
837
1010
|
candidates.push({ ...definition, resolvedExecutable });
|
|
838
1011
|
}
|