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