@orkestrel/browser 0.0.13 → 0.0.15
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 +3 -3
- package/dist/src/core/index.cjs +2574 -2355
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1185 -720
- package/dist/src/core/index.d.ts +1185 -720
- package/dist/src/core/index.js +2544 -2322
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +317 -215
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +198 -118
- package/dist/src/server/index.d.ts +198 -118
- package/dist/src/server/index.js +310 -206
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -15
package/dist/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_WAIT_POLL_INTERVAL_MS, BrowserContext, BrowserError, CDPClient, validateBrowserContextOptions } from "../core/index.js";
|
|
1
|
+
import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_WAIT_POLL_INTERVAL_MS, BrowserContext, BrowserError, BrowserTransition, CDPClient, validateBrowserContextOptions } from "../core/index.js";
|
|
2
2
|
import { attempt, isArray, isError, isInstance, isInteger, isRecord, isString } from "@orkestrel/contract";
|
|
3
3
|
import { existsSync, globSync } from "node:fs";
|
|
4
4
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
@@ -13,35 +13,35 @@ import { Emitter } from "@orkestrel/emitter";
|
|
|
13
13
|
import { WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
|
|
14
14
|
import { addAbortListener, once } from "node:events";
|
|
15
15
|
//#region src/server/constants.ts
|
|
16
|
-
/**
|
|
16
|
+
/** Sets the default CDP port probed for an existing browser and used for launches. */
|
|
17
17
|
var BROWSER_DEFAULT_CDP_PORT = 9222;
|
|
18
|
-
/**
|
|
18
|
+
/** Sets the default host probed for an existing browser and used for launches (avoids `localhost` resolving to `::1` when Chromium binds `127.0.0.1`). */
|
|
19
19
|
var BROWSER_DEFAULT_HOST = "127.0.0.1";
|
|
20
|
-
/**
|
|
20
|
+
/** Names the protocol prefix for CDP discovery requests. */
|
|
21
21
|
var BROWSER_CDP_PROTOCOL = "http";
|
|
22
|
-
/**
|
|
22
|
+
/** Names the path appended to the CDP host to fetch version metadata (endpoint discovery). */
|
|
23
23
|
var BROWSER_CDP_VERSION_PATH = "/json/version";
|
|
24
|
-
/**
|
|
24
|
+
/** Names the path appended to the CDP host to list open targets (pages, workers, etc). */
|
|
25
25
|
var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
26
|
-
/**
|
|
26
|
+
/** Lists the flags always passed to a launched browser process, alongside the caller's own. */
|
|
27
27
|
var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
|
|
28
|
-
/**
|
|
28
|
+
/** Names the flag that enables headless mode on a launched browser process. */
|
|
29
29
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
30
|
-
/**
|
|
30
|
+
/** Names the prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
31
31
|
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
32
|
-
/**
|
|
32
|
+
/** Bounds each launched-process exit window during TERM-to-KILL teardown. */
|
|
33
33
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
34
|
-
/**
|
|
34
|
+
/** Bounds the `discover: false` port-occupancy probe before launching — short, because it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
|
|
35
35
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
36
|
-
/**
|
|
36
|
+
/** Defers once, briefly, when a transport loss is observed on an owned process, giving a near-simultaneous process-exit event (which libuv may reap slightly later than the socket close) first say over the diagnosis. */
|
|
37
37
|
var BROWSER_TRANSPORT_LOSS_DEFER_MS = 50;
|
|
38
|
-
/**
|
|
38
|
+
/** Names the machine-readable error-context cause for an owned browser process exiting. */
|
|
39
39
|
var BROWSER_PROCESS_EXIT_CAUSE = "process-exit";
|
|
40
|
-
/**
|
|
40
|
+
/** Names the machine-readable error-context cause for a CDP transport disconnecting while its browser remains alive. */
|
|
41
41
|
var BROWSER_TRANSPORT_LOSS_CAUSE = "transport-loss";
|
|
42
|
-
/**
|
|
42
|
+
/** Lists the environment variables checked (in order) for an explicit browser executable path override. */
|
|
43
43
|
var BROWSER_ENV_PATH_KEYS = Object.freeze(["PLAYWRIGHT_EXECUTABLE_PATH", "CHROME_PATH"]);
|
|
44
|
-
/**
|
|
44
|
+
/** Lists the well-known Chrome/Chromium/Edge executable paths with no platform-specific root, keyed by `process.platform`. */
|
|
45
45
|
var BROWSER_EXECUTABLE_PATHS = Object.freeze({
|
|
46
46
|
linux: Object.freeze([
|
|
47
47
|
"/usr/bin/google-chrome",
|
|
@@ -61,18 +61,18 @@ var BROWSER_EXECUTABLE_PATHS = Object.freeze({
|
|
|
61
61
|
]),
|
|
62
62
|
win32: Object.freeze([])
|
|
63
63
|
});
|
|
64
|
-
/** Windows install-root-relative suffixes for Chrome/Edge/Chromium, joined against each candidate root (`PROGRAMFILES`, `PROGRAMFILES(X86)`, `LOCALAPPDATA`). */
|
|
64
|
+
/** Lists the Windows install-root-relative suffixes for Chrome/Edge/Chromium, joined against each candidate root (`PROGRAMFILES`, `PROGRAMFILES(X86)`, `LOCALAPPDATA`). */
|
|
65
65
|
var BROWSER_WINDOWS_SUFFIXES = Object.freeze([
|
|
66
66
|
"Google\\Chrome\\Application\\chrome.exe",
|
|
67
67
|
"Microsoft\\Edge\\Application\\msedge.exe",
|
|
68
68
|
"Chromium\\Application\\chrome.exe"
|
|
69
69
|
]);
|
|
70
|
-
/**
|
|
70
|
+
/** Lists the fallback Windows install roots used when the corresponding environment variable is absent. */
|
|
71
71
|
var BROWSER_WINDOWS_ROOT_FALLBACKS = Object.freeze({
|
|
72
72
|
PROGRAMFILES: "C:\\Program Files",
|
|
73
73
|
"PROGRAMFILES(X86)": "C:\\Program Files (x86)"
|
|
74
74
|
});
|
|
75
|
-
/**
|
|
75
|
+
/** Lists the command names probed on PATH when no well-known executable path exists. */
|
|
76
76
|
var BROWSER_EXECUTABLE_NAMES = Object.freeze([
|
|
77
77
|
"google-chrome",
|
|
78
78
|
"google-chrome-stable",
|
|
@@ -82,19 +82,19 @@ var BROWSER_EXECUTABLE_NAMES = Object.freeze([
|
|
|
82
82
|
"chromium-browser",
|
|
83
83
|
"chrome"
|
|
84
84
|
]);
|
|
85
|
-
/**
|
|
85
|
+
/** Names the environment variable that carries an additional Playwright browser store base directory. */
|
|
86
86
|
var BROWSER_STORE_ENV_KEY = "PLAYWRIGHT_BROWSERS_PATH";
|
|
87
|
-
/**
|
|
87
|
+
/** Lists the well-known Playwright browser store base directories checked in addition to `PLAYWRIGHT_BROWSERS_PATH`. */
|
|
88
88
|
var BROWSER_STORE_DEFAULT_DIRS = Object.freeze(["/opt/pw-browsers"]);
|
|
89
|
-
/**
|
|
89
|
+
/** Names the per-OS default Playwright browser cache directory, relative to the home directory (win32 uses `LOCALAPPDATA` directly). */
|
|
90
90
|
var BROWSER_STORE_CACHE_DIRS = Object.freeze({
|
|
91
91
|
linux: ".cache/ms-playwright",
|
|
92
92
|
darwin: "Library/Caches/ms-playwright"
|
|
93
93
|
});
|
|
94
|
-
/**
|
|
94
|
+
/** Names the top-level Chromium symlink/binary Playwright maintains inside a browser store base. */
|
|
95
95
|
var BROWSER_STORE_LINK_NAME = "chromium";
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* Lists the case-insensitive substrings identifying an executable path/name's browser
|
|
98
98
|
* engine, checked by `parseBrowserEngine` in the order `edge` → `chromium` → `chrome`.
|
|
99
99
|
*/
|
|
100
100
|
var BROWSER_ENGINE_HINTS = Object.freeze({
|
|
@@ -118,7 +118,7 @@ var BROWSER_ENGINE_HINTS = Object.freeze({
|
|
|
118
118
|
"chrome"
|
|
119
119
|
])
|
|
120
120
|
});
|
|
121
|
-
/**
|
|
121
|
+
/** Names the glob pattern (relative to a store base) matching a versioned Chromium binary, keyed by `process.platform`. */
|
|
122
122
|
var BROWSER_STORE_GLOBS = Object.freeze({
|
|
123
123
|
linux: "chromium-*/chrome-linux*/chrome",
|
|
124
124
|
darwin: "chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
|
|
@@ -127,7 +127,7 @@ var BROWSER_STORE_GLOBS = Object.freeze({
|
|
|
127
127
|
//#endregion
|
|
128
128
|
//#region src/server/errors.ts
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
130
|
+
* Reports that a CDP connection, discovery, or launch attempt failed.
|
|
131
131
|
*/
|
|
132
132
|
var BrowserConnectionError = class extends BrowserError {
|
|
133
133
|
constructor(message, context) {
|
|
@@ -136,7 +136,7 @@ var BrowserConnectionError = class extends BrowserError {
|
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
139
|
+
* Reports that an operation requiring an active connection was attempted while disconnected.
|
|
140
140
|
*/
|
|
141
141
|
var BrowserNotConnectedError = class extends BrowserError {
|
|
142
142
|
constructor(context) {
|
|
@@ -145,7 +145,7 @@ var BrowserNotConnectedError = class extends BrowserError {
|
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
/**
|
|
148
|
-
*
|
|
148
|
+
* Reports that an operation was attempted after the Browser was destroyed.
|
|
149
149
|
*/
|
|
150
150
|
var BrowserDestroyedError = class extends BrowserError {
|
|
151
151
|
constructor(context) {
|
|
@@ -154,28 +154,28 @@ var BrowserDestroyedError = class extends BrowserError {
|
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* Narrows an unknown value to BrowserConnectionError.
|
|
158
158
|
*
|
|
159
159
|
* @param value - Value to check
|
|
160
|
-
* @returns True
|
|
160
|
+
* @returns True if value is a BrowserConnectionError instance; false otherwise
|
|
161
161
|
*/
|
|
162
162
|
function isBrowserConnectionError(value) {
|
|
163
163
|
return isInstance(value, BrowserConnectionError);
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
166
|
+
* Narrows an unknown value to BrowserNotConnectedError.
|
|
167
167
|
*
|
|
168
168
|
* @param value - Value to check
|
|
169
|
-
* @returns True
|
|
169
|
+
* @returns True if value is a BrowserNotConnectedError instance; false otherwise
|
|
170
170
|
*/
|
|
171
171
|
function isBrowserNotConnectedError(value) {
|
|
172
172
|
return isInstance(value, BrowserNotConnectedError);
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
175
|
+
* Narrows an unknown value to BrowserDestroyedError.
|
|
176
176
|
*
|
|
177
177
|
* @param value - Value to check
|
|
178
|
-
* @returns True
|
|
178
|
+
* @returns True if value is a BrowserDestroyedError instance; false otherwise
|
|
179
179
|
*/
|
|
180
180
|
function isBrowserDestroyedError(value) {
|
|
181
181
|
return isInstance(value, BrowserDestroyedError);
|
|
@@ -183,7 +183,7 @@ function isBrowserDestroyedError(value) {
|
|
|
183
183
|
//#endregion
|
|
184
184
|
//#region src/server/helpers.ts
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
186
|
+
* Enumerates every Chrome/Chromium/Edge executable discoverable on this
|
|
187
187
|
* machine, deduplicated by normalized absolute path.
|
|
188
188
|
*
|
|
189
189
|
* @remarks
|
|
@@ -197,7 +197,7 @@ function isBrowserDestroyedError(value) {
|
|
|
197
197
|
* and the per-OS Playwright cache directory) — the top-level `chromium`
|
|
198
198
|
* link/binary, then the highest-revision `chromium-*` install found
|
|
199
199
|
*
|
|
200
|
-
* Each candidate is classified into a {@link BrowserEngine}
|
|
200
|
+
* Each candidate is classified into a {@link BrowserEngine} through
|
|
201
201
|
* `parseBrowserEngine` (unclassifiable candidates default to `'chromium'`),
|
|
202
202
|
* and `options.engine` (when given) narrows the result to that engine.
|
|
203
203
|
*
|
|
@@ -208,13 +208,13 @@ function findSystemBrowsers(options) {
|
|
|
208
208
|
const platform = process.platform;
|
|
209
209
|
const env = options?.env ?? process.env;
|
|
210
210
|
const candidates = [];
|
|
211
|
-
candidates.push(...
|
|
212
|
-
const paths = options?.paths ??
|
|
213
|
-
candidates.push(...
|
|
211
|
+
candidates.push(...findEnvOverrides(env));
|
|
212
|
+
const paths = options?.paths ?? buildInstallPaths(platform, env);
|
|
213
|
+
candidates.push(...findInstallPaths(paths));
|
|
214
214
|
const names = options?.names ?? BROWSER_EXECUTABLE_NAMES;
|
|
215
|
-
candidates.push(...
|
|
216
|
-
const stores = options?.stores ??
|
|
217
|
-
for (const store of stores) candidates.push(...
|
|
215
|
+
candidates.push(...probePathNames(names, platform));
|
|
216
|
+
const stores = options?.stores ?? buildStoreBases(env, platform);
|
|
217
|
+
for (const store of stores) candidates.push(...findStorePaths(store, platform));
|
|
218
218
|
const seen = /* @__PURE__ */ new Set();
|
|
219
219
|
const browsers = [];
|
|
220
220
|
for (const executable of candidates) {
|
|
@@ -231,7 +231,7 @@ function findSystemBrowsers(options) {
|
|
|
231
231
|
return browsers;
|
|
232
232
|
}
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
234
|
+
* Locates a Chrome/Chromium/Edge executable on this machine — the first entry
|
|
235
235
|
* of {@link findSystemBrowsers}.
|
|
236
236
|
*
|
|
237
237
|
* @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
|
|
@@ -241,7 +241,7 @@ function findSystemBrowser(options) {
|
|
|
241
241
|
return findSystemBrowsers(options)[0];
|
|
242
242
|
}
|
|
243
243
|
/**
|
|
244
|
-
*
|
|
244
|
+
* Classifies an executable path/name into a {@link BrowserEngine} by
|
|
245
245
|
* case-insensitive hint, checked in the order edge → chromium → chrome.
|
|
246
246
|
*
|
|
247
247
|
* @param executable - Executable path or command name to classify
|
|
@@ -255,19 +255,19 @@ function parseBrowserEngine(executable) {
|
|
|
255
255
|
"chrome"
|
|
256
256
|
]) if (BROWSER_ENGINE_HINTS[engine].some((hint) => lower.includes(hint))) return engine;
|
|
257
257
|
}
|
|
258
|
-
/**
|
|
258
|
+
/** Normalizes an executable path for cross-source deduplication (case-insensitive on Windows). */
|
|
259
259
|
function normalizeExecutablePath(path, platform) {
|
|
260
260
|
const normalized = platform === "win32" ? win32.normalize(path) : posix.normalize(path);
|
|
261
261
|
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
262
262
|
}
|
|
263
|
-
/**
|
|
263
|
+
/** Classifies a `/json/version` `Browser` string into a {@link BrowserEngine} (`Edg/` → edge, `Chrome/` → chrome, else chromium). */
|
|
264
264
|
function browserToEngine(browser) {
|
|
265
265
|
if (browser?.includes("Edg/") === true) return "edge";
|
|
266
266
|
if (browser?.includes("Chrome/") === true) return "chrome";
|
|
267
267
|
return "chromium";
|
|
268
268
|
}
|
|
269
269
|
/**
|
|
270
|
-
*
|
|
270
|
+
* Resolves a persistent caller profile or creates an isolated temporary one.
|
|
271
271
|
*
|
|
272
272
|
* @param path - Optional caller-owned persistent user-data directory
|
|
273
273
|
* @returns The resolved profile and whether the library owns its lifecycle
|
|
@@ -283,7 +283,7 @@ async function createBrowserProfile(path) {
|
|
|
283
283
|
};
|
|
284
284
|
}
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
286
|
+
* Removes a library-owned isolated browser profile.
|
|
287
287
|
*
|
|
288
288
|
* @remarks
|
|
289
289
|
* The path must remain a direct child of the operating-system temp directory
|
|
@@ -296,7 +296,7 @@ async function removeBrowserProfile(profile) {
|
|
|
296
296
|
if (!profile.temporary) return;
|
|
297
297
|
const path = resolve(profile.path);
|
|
298
298
|
const temporary = resolve(tmpdir());
|
|
299
|
-
if (dirname(path) !== temporary || !basename(path).startsWith("orkestrel-browser-")) throw new
|
|
299
|
+
if (dirname(path) !== temporary || !basename(path).startsWith("orkestrel-browser-")) throw new BrowserError("Refusing to remove an unsafe browser profile path", void 0, { path });
|
|
300
300
|
await rm(path, {
|
|
301
301
|
recursive: true,
|
|
302
302
|
force: true,
|
|
@@ -304,12 +304,8 @@ async function removeBrowserProfile(profile) {
|
|
|
304
304
|
retryDelay: 100
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
|
-
/**
|
|
308
|
-
function
|
|
309
|
-
return findAllEnvOverrides(env)[0];
|
|
310
|
-
}
|
|
311
|
-
/** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order, returning every one that exists. */
|
|
312
|
-
function findAllEnvOverrides(env) {
|
|
307
|
+
/** Checks the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order and returns every one that exists. */
|
|
308
|
+
function findEnvOverrides(env) {
|
|
313
309
|
const found = [];
|
|
314
310
|
for (const key of BROWSER_ENV_PATH_KEYS) {
|
|
315
311
|
const value = env[key];
|
|
@@ -317,49 +313,64 @@ function findAllEnvOverrides(env) {
|
|
|
317
313
|
}
|
|
318
314
|
return found;
|
|
319
315
|
}
|
|
320
|
-
/**
|
|
321
|
-
function
|
|
316
|
+
/** Builds the default well-known install-path candidates for a platform, deriving Windows roots from env vars. */
|
|
317
|
+
function buildInstallPaths(platform, env) {
|
|
322
318
|
if (platform !== "win32") return BROWSER_EXECUTABLE_PATHS[platform] ?? [];
|
|
323
|
-
const roots =
|
|
319
|
+
const roots = buildWindowsRoots(env);
|
|
324
320
|
const paths = [];
|
|
325
321
|
for (const root of roots) for (const suffix of BROWSER_WINDOWS_SUFFIXES) paths.push(win32.join(root, suffix));
|
|
326
322
|
return paths;
|
|
327
323
|
}
|
|
328
|
-
/**
|
|
329
|
-
function
|
|
324
|
+
/** Derives Windows install roots from env vars, falling back to well-known literals when absent. */
|
|
325
|
+
function buildWindowsRoots(env) {
|
|
330
326
|
return [
|
|
331
327
|
env["PROGRAMFILES"] ?? BROWSER_WINDOWS_ROOT_FALLBACKS["PROGRAMFILES"],
|
|
332
328
|
env["PROGRAMFILES(X86)"] ?? BROWSER_WINDOWS_ROOT_FALLBACKS["PROGRAMFILES(X86)"],
|
|
333
329
|
env["LOCALAPPDATA"]
|
|
334
330
|
].filter((root) => root !== void 0);
|
|
335
331
|
}
|
|
336
|
-
/**
|
|
337
|
-
function
|
|
338
|
-
return findAllInstallPaths(paths)[0];
|
|
339
|
-
}
|
|
340
|
-
/** Return every candidate path that exists on disk, in the given order. */
|
|
341
|
-
function findAllInstallPaths(paths) {
|
|
332
|
+
/** Returns every candidate path that exists on disk, in the given order. */
|
|
333
|
+
function findInstallPaths(paths) {
|
|
342
334
|
return paths.filter((path) => existsSync(path));
|
|
343
335
|
}
|
|
344
|
-
/**
|
|
336
|
+
/** Probes PATH (`which`/`where`) for every resolvable command name, in the given order. */
|
|
345
337
|
function probePathNames(names, platform) {
|
|
346
|
-
return probeAllPathNames(names, platform)[0];
|
|
347
|
-
}
|
|
348
|
-
/** Probe PATH (`which`/`where`) for every resolvable command name, in the given order. */
|
|
349
|
-
function probeAllPathNames(names, platform) {
|
|
350
338
|
const finder = platform === "win32" ? "where" : "which";
|
|
351
339
|
const found = [];
|
|
352
340
|
for (const name of names) {
|
|
353
341
|
const result = spawnSync(finder, [name], { stdio: "pipe" });
|
|
354
342
|
if (result.status === 0) {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
343
|
+
const executable = readFirstLine(result.stdout.toString("utf-8"));
|
|
344
|
+
if (executable !== void 0) found.push(executable);
|
|
357
345
|
}
|
|
358
346
|
}
|
|
359
347
|
return found;
|
|
360
348
|
}
|
|
361
|
-
/**
|
|
362
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Returns the first non-empty line of a command's output, without its
|
|
351
|
+
* surrounding whitespace.
|
|
352
|
+
*
|
|
353
|
+
* @remarks
|
|
354
|
+
* `where` reports every PATH match on its own CRLF-terminated line, so a
|
|
355
|
+
* carriage return left on the first line would travel into `existsSync` and
|
|
356
|
+
* `spawn` as part of the executable path.
|
|
357
|
+
*
|
|
358
|
+
* @param output - Raw decoded standard output of a command
|
|
359
|
+
* @returns The first line carrying text, or undefined when the output has none
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* readFirstLine('C:\\bin\\chrome.exe\r\nC:\\other\\chrome.exe\r\n') // 'C:\\bin\\chrome.exe'
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
function readFirstLine(output) {
|
|
367
|
+
for (const line of output.split(/\r?\n/)) {
|
|
368
|
+
const text = line.trim();
|
|
369
|
+
if (text.length > 0) return text;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Builds the default Playwright browser store base directories to search for a managed Chromium. */
|
|
373
|
+
function buildStoreBases(env, platform) {
|
|
363
374
|
const bases = [];
|
|
364
375
|
const envBase = env[BROWSER_STORE_ENV_KEY];
|
|
365
376
|
if (envBase !== void 0 && envBase.length > 0) bases.push(envBase);
|
|
@@ -374,12 +385,8 @@ function defaultStoreBases(env, platform) {
|
|
|
374
385
|
}
|
|
375
386
|
return bases;
|
|
376
387
|
}
|
|
377
|
-
/**
|
|
378
|
-
function
|
|
379
|
-
return findAllInStore(base, platform)[0];
|
|
380
|
-
}
|
|
381
|
-
/** Search one store base for the top-level `chromium` link and every `chromium-*` install, highest revision first. */
|
|
382
|
-
function findAllInStore(base, platform) {
|
|
388
|
+
/** Searches one store base for the top-level `chromium` link and every `chromium-*` install, highest revision first. */
|
|
389
|
+
function findStorePaths(base, platform) {
|
|
383
390
|
const joiner = platform === "win32" ? win32.join : posix.join;
|
|
384
391
|
const found = [];
|
|
385
392
|
const linkPath = joiner(base, BROWSER_STORE_LINK_NAME);
|
|
@@ -393,11 +400,16 @@ function findAllInStore(base, platform) {
|
|
|
393
400
|
return found;
|
|
394
401
|
}
|
|
395
402
|
/**
|
|
396
|
-
*
|
|
403
|
+
* Launches a browser process with raw-CDP debugging flags.
|
|
397
404
|
*
|
|
398
405
|
* @remarks
|
|
399
406
|
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
400
407
|
* signal and await every Chromium subprocess without affecting the caller.
|
|
408
|
+
* Windows launches are not detached and own no group, so teardown there
|
|
409
|
+
* signals one process by identifier: the spawned process, or the process the
|
|
410
|
+
* launcher handed the endpoint to when that spawned process re-executed the
|
|
411
|
+
* browser and exited. Terminating a Chromium browser process takes its own
|
|
412
|
+
* subprocesses with it, so that single signal drains the tree.
|
|
401
413
|
*
|
|
402
414
|
* @param executable - Absolute path to the browser executable
|
|
403
415
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
@@ -418,11 +430,12 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
418
430
|
});
|
|
419
431
|
}
|
|
420
432
|
/**
|
|
421
|
-
*
|
|
433
|
+
* Polls a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
422
434
|
*
|
|
423
435
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
424
436
|
* @param timeout - Maximum time to wait in milliseconds
|
|
425
437
|
* @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
|
|
438
|
+
* @param signal - Optional external abort; an abort while waiting rethrows rather than resolving
|
|
426
439
|
* @returns The browser's WebSocket debugger URL
|
|
427
440
|
*
|
|
428
441
|
* @throws When the endpoint does not become ready before the timeout
|
|
@@ -446,15 +459,33 @@ async function waitForCDPReady(port, timeout, host = BROWSER_DEFAULT_HOST, signa
|
|
|
446
459
|
const delay = Math.min(BROWSER_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
447
460
|
if (delay > 0) await setTimeout$1(delay, void 0, { signal });
|
|
448
461
|
}
|
|
449
|
-
throw new
|
|
462
|
+
throw new BrowserConnectionError(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`, {
|
|
463
|
+
port,
|
|
464
|
+
timeout
|
|
465
|
+
});
|
|
450
466
|
}
|
|
451
467
|
/**
|
|
452
|
-
*
|
|
468
|
+
* Fetches the current CDP target list from a browser's `/json/list` endpoint.
|
|
469
|
+
*
|
|
470
|
+
* @remarks
|
|
471
|
+
* The endpoint is a network boundary, so an unreachable host, a non-2xx
|
|
472
|
+
* response, and a body that is not a JSON array each come back as a failed
|
|
473
|
+
* `Result` carrying a coded `BrowserConnectionError`. An entry missing a
|
|
474
|
+
* required string field is skipped rather than failing the whole list.
|
|
453
475
|
*
|
|
454
476
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
455
477
|
* @param timeout - Request timeout in milliseconds
|
|
456
478
|
* @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
|
|
457
|
-
* @returns
|
|
479
|
+
* @returns The normalized CDP targets, or the fault that stopped the request
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* import { fetchCDPTargets } from '@orkestrel/browser/server'
|
|
484
|
+
*
|
|
485
|
+
* const result = await fetchCDPTargets(9222, 2000)
|
|
486
|
+
* if (result.success) log(result.value.length)
|
|
487
|
+
* else log(result.error.code)
|
|
488
|
+
* ```
|
|
458
489
|
*/
|
|
459
490
|
async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
460
491
|
const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_LIST_PATH}`;
|
|
@@ -462,23 +493,41 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
462
493
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
463
494
|
try {
|
|
464
495
|
const response = await fetch(url, { signal: controller.signal });
|
|
465
|
-
if (!response.ok) return
|
|
496
|
+
if (!response.ok) return {
|
|
497
|
+
success: false,
|
|
498
|
+
error: new BrowserConnectionError("CDP target list request was refused", {
|
|
499
|
+
url,
|
|
500
|
+
status: response.status
|
|
501
|
+
})
|
|
502
|
+
};
|
|
466
503
|
const list = await response.json();
|
|
467
|
-
if (!isArray(list)) return
|
|
504
|
+
if (!isArray(list)) return {
|
|
505
|
+
success: false,
|
|
506
|
+
error: new BrowserConnectionError("CDP target list is not a JSON array", { url })
|
|
507
|
+
};
|
|
468
508
|
const targets = [];
|
|
469
509
|
for (const entry of list) {
|
|
470
510
|
if (!isRecord(entry)) continue;
|
|
471
511
|
if (!isString(entry["id"]) || !isString(entry["type"]) || !isString(entry["title"]) || !isString(entry["url"])) continue;
|
|
472
512
|
targets.push({
|
|
473
513
|
id: entry["id"],
|
|
474
|
-
|
|
514
|
+
category: entry["type"],
|
|
475
515
|
title: entry["title"],
|
|
476
516
|
url: entry["url"]
|
|
477
517
|
});
|
|
478
518
|
}
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
519
|
+
return {
|
|
520
|
+
success: true,
|
|
521
|
+
value: targets
|
|
522
|
+
};
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error: new BrowserConnectionError("CDP target list is unreachable", {
|
|
527
|
+
url,
|
|
528
|
+
error
|
|
529
|
+
})
|
|
530
|
+
};
|
|
482
531
|
} finally {
|
|
483
532
|
clearTimeout(timer);
|
|
484
533
|
}
|
|
@@ -486,27 +535,36 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
486
535
|
//#endregion
|
|
487
536
|
//#region src/server/transports/WebSocketCDPTransport.ts
|
|
488
537
|
/**
|
|
489
|
-
*
|
|
538
|
+
* Provides a raw CDP text transport backed by `@orkestrel/websocket`.
|
|
490
539
|
*
|
|
491
540
|
* @remarks
|
|
492
541
|
* Performs and validates the RFC 6455 HTTP upgrade, then delegates frame
|
|
493
542
|
* handling to `@orkestrel/websocket`. Concurrent and repeated `start()` /
|
|
494
543
|
* `close()` calls share their active transition, and a later `start()` opens a
|
|
495
544
|
* fresh socket after the prior one closes.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* import { WebSocketCDPTransport } from '@orkestrel/browser/server'
|
|
549
|
+
*
|
|
550
|
+
* const transport = new WebSocketCDPTransport({ url: 'ws://127.0.0.1:9222/devtools/browser/abc' })
|
|
551
|
+
* await transport.start()
|
|
552
|
+
* await transport.close()
|
|
553
|
+
* ```
|
|
496
554
|
*/
|
|
497
555
|
var WebSocketCDPTransport = class {
|
|
498
556
|
#emitter;
|
|
499
557
|
#url;
|
|
500
558
|
#timeout;
|
|
501
559
|
#socket;
|
|
502
|
-
#starting;
|
|
503
|
-
#closing;
|
|
560
|
+
#starting = new BrowserTransition();
|
|
561
|
+
#closing = new BrowserTransition();
|
|
504
562
|
#request;
|
|
505
563
|
#resolve;
|
|
506
564
|
#reject;
|
|
507
565
|
#timer;
|
|
508
566
|
#closeResolve;
|
|
509
|
-
#
|
|
567
|
+
#socketCloseHandler = this.#handleSocketClose.bind(this);
|
|
510
568
|
constructor(options) {
|
|
511
569
|
this.#emitter = new Emitter({
|
|
512
570
|
...options.on !== void 0 ? { on: options.on } : {},
|
|
@@ -519,43 +577,27 @@ var WebSocketCDPTransport = class {
|
|
|
519
577
|
return this.#emitter;
|
|
520
578
|
}
|
|
521
579
|
async start() {
|
|
522
|
-
|
|
580
|
+
const closing = this.#closing.pending;
|
|
581
|
+
if (closing !== void 0) await closing;
|
|
523
582
|
if (this.#socket?.readyState === WEBSOCKET_READY_OPEN) return;
|
|
524
|
-
const active = this.#starting;
|
|
583
|
+
const active = this.#starting.pending;
|
|
525
584
|
if (active !== void 0) {
|
|
526
585
|
await active;
|
|
527
586
|
return;
|
|
528
587
|
}
|
|
529
588
|
this.#socket?.destroy();
|
|
530
589
|
this.#socket = void 0;
|
|
531
|
-
|
|
532
|
-
this.#starting = transition;
|
|
533
|
-
try {
|
|
534
|
-
await transition;
|
|
535
|
-
} finally {
|
|
536
|
-
if (this.#starting === transition) this.#starting = void 0;
|
|
537
|
-
}
|
|
590
|
+
await this.#starting.execute(() => this.#start());
|
|
538
591
|
}
|
|
539
592
|
async send(data) {
|
|
540
593
|
const socket = this.#socket;
|
|
541
|
-
if (socket === void 0 || socket.readyState !== WEBSOCKET_READY_OPEN) throw new
|
|
594
|
+
if (socket === void 0 || socket.readyState !== WEBSOCKET_READY_OPEN) throw new BrowserConnectionError("WebSocket CDP transport is not open", { url: this.#url });
|
|
542
595
|
socket.send(data);
|
|
543
596
|
}
|
|
544
597
|
async close() {
|
|
545
|
-
|
|
546
|
-
if (active !== void 0) {
|
|
547
|
-
await active;
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
const transition = this.#stop();
|
|
551
|
-
this.#closing = transition;
|
|
552
|
-
try {
|
|
553
|
-
await transition;
|
|
554
|
-
} finally {
|
|
555
|
-
if (this.#closing === transition) this.#closing = void 0;
|
|
556
|
-
}
|
|
598
|
+
await this.#closing.execute(() => this.#close());
|
|
557
599
|
}
|
|
558
|
-
async #
|
|
600
|
+
async #start() {
|
|
559
601
|
const parsed = attempt(() => new URL(this.#url));
|
|
560
602
|
if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
561
603
|
url: this.#url,
|
|
@@ -591,19 +633,19 @@ var WebSocketCDPTransport = class {
|
|
|
591
633
|
this.#socket = socket;
|
|
592
634
|
this.#bind(socket);
|
|
593
635
|
}
|
|
594
|
-
async #
|
|
636
|
+
async #close() {
|
|
595
637
|
const request = this.#request;
|
|
596
638
|
if (request !== void 0) {
|
|
597
639
|
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
598
640
|
request.destroy();
|
|
599
641
|
}
|
|
600
|
-
await this.#starting?.catch(() => void 0);
|
|
642
|
+
await this.#starting.pending?.catch(() => void 0);
|
|
601
643
|
const socket = this.#socket;
|
|
602
644
|
this.#socket = void 0;
|
|
603
645
|
if (socket === void 0 || socket.readyState === WEBSOCKET_READY_CLOSED) return;
|
|
604
646
|
const closed = Promise.withResolvers();
|
|
605
647
|
this.#closeResolve = closed.resolve;
|
|
606
|
-
socket.emitter.on("close", this.#
|
|
648
|
+
socket.emitter.on("close", this.#socketCloseHandler);
|
|
607
649
|
try {
|
|
608
650
|
socket.close();
|
|
609
651
|
if (socket.readyState !== WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
@@ -611,7 +653,7 @@ var WebSocketCDPTransport = class {
|
|
|
611
653
|
socket.destroy();
|
|
612
654
|
if (socket.readyState !== WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
613
655
|
} finally {
|
|
614
|
-
socket.emitter.off("close", this.#
|
|
656
|
+
socket.emitter.off("close", this.#socketCloseHandler);
|
|
615
657
|
this.#closeResolve = void 0;
|
|
616
658
|
}
|
|
617
659
|
}
|
|
@@ -686,16 +728,46 @@ var WebSocketCDPTransport = class {
|
|
|
686
728
|
}
|
|
687
729
|
};
|
|
688
730
|
//#endregion
|
|
731
|
+
//#region src/server/writers/FileBrowserWriter.ts
|
|
732
|
+
/**
|
|
733
|
+
* Persists captured browser bytes to the filesystem through `node:fs/promises`.
|
|
734
|
+
*
|
|
735
|
+
* @remarks
|
|
736
|
+
* Creates every missing parent directory of the target path before writing, so
|
|
737
|
+
* a caller may name a nested destination that does not exist yet.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```ts
|
|
741
|
+
* import { FileBrowserWriter } from '@orkestrel/browser/server'
|
|
742
|
+
*
|
|
743
|
+
* const writer = new FileBrowserWriter()
|
|
744
|
+
* await writer.write('shots/hero.png', new Uint8Array([137, 80, 78, 71]))
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
var FileBrowserWriter = class {
|
|
748
|
+
/**
|
|
749
|
+
* Writes the captured bytes to a path, creating its parent directories.
|
|
750
|
+
*
|
|
751
|
+
* @param path - Destination file path
|
|
752
|
+
* @param data - Captured bytes to persist
|
|
753
|
+
* @returns A promise resolving once the bytes are on disk
|
|
754
|
+
*/
|
|
755
|
+
async write(path, data) {
|
|
756
|
+
await mkdir(dirname(path), { recursive: true });
|
|
757
|
+
await writeFile(path, data);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
//#endregion
|
|
689
761
|
//#region src/server/factories.ts
|
|
690
762
|
/**
|
|
691
|
-
*
|
|
763
|
+
* Creates a raw-CDP Browser façade.
|
|
692
764
|
*
|
|
693
765
|
* @param options - Connection, launch, and viewport configuration
|
|
694
766
|
* @returns A {@link BrowserInterface}
|
|
695
767
|
*
|
|
696
768
|
* @example
|
|
697
769
|
* ```ts
|
|
698
|
-
* import { createBrowser } from '@
|
|
770
|
+
* import { createBrowser } from '@orkestrel/browser/server'
|
|
699
771
|
*
|
|
700
772
|
* const browser = createBrowser()
|
|
701
773
|
* await browser.connect()
|
|
@@ -705,7 +777,7 @@ function createBrowser(options) {
|
|
|
705
777
|
return new Browser(options);
|
|
706
778
|
}
|
|
707
779
|
/**
|
|
708
|
-
*
|
|
780
|
+
* Creates a Node `WebSocket`-backed CDP transport.
|
|
709
781
|
*
|
|
710
782
|
* @param options - The CDP WebSocket debugger URL (and optional timeout)
|
|
711
783
|
* @returns A {@link CDPTransportInterface}
|
|
@@ -714,20 +786,27 @@ function createCDPTransport(options) {
|
|
|
714
786
|
return new WebSocketCDPTransport(options);
|
|
715
787
|
}
|
|
716
788
|
/**
|
|
717
|
-
*
|
|
789
|
+
* Creates a filesystem-backed browser writer.
|
|
718
790
|
*
|
|
719
|
-
* @returns A {@link
|
|
791
|
+
* @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
|
|
720
792
|
*/
|
|
721
|
-
function
|
|
722
|
-
return
|
|
723
|
-
await mkdir(dirname(path), { recursive: true });
|
|
724
|
-
await writeFile(path, data);
|
|
725
|
-
} };
|
|
793
|
+
function createBrowserWriter() {
|
|
794
|
+
return new FileBrowserWriter();
|
|
726
795
|
}
|
|
727
796
|
//#endregion
|
|
728
797
|
//#region src/server/Browser.ts
|
|
729
798
|
/**
|
|
730
799
|
* Discovers, launches, connects to, and owns Chromium-family browser sessions.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* ```ts
|
|
803
|
+
* import { Browser } from '@orkestrel/browser/server'
|
|
804
|
+
*
|
|
805
|
+
* const browser = new Browser({ headless: true })
|
|
806
|
+
* await browser.connect()
|
|
807
|
+
* const page = await browser.create({ url: 'https://example.com' })
|
|
808
|
+
* await browser.destroy()
|
|
809
|
+
* ```
|
|
731
810
|
*/
|
|
732
811
|
var Browser = class {
|
|
733
812
|
#emitter;
|
|
@@ -742,17 +821,18 @@ var Browser = class {
|
|
|
742
821
|
#endpoint;
|
|
743
822
|
#client;
|
|
744
823
|
#process;
|
|
824
|
+
#servingPid;
|
|
745
825
|
#profile;
|
|
746
826
|
#contexts = [];
|
|
747
827
|
#destroyed = false;
|
|
748
|
-
#connecting;
|
|
749
|
-
#disconnecting;
|
|
828
|
+
#connecting = new BrowserTransition();
|
|
829
|
+
#disconnecting = new BrowserTransition();
|
|
750
830
|
#exitCleanup;
|
|
751
831
|
#shutdown;
|
|
752
832
|
#transport;
|
|
753
|
-
#
|
|
754
|
-
#
|
|
755
|
-
#
|
|
833
|
+
#transportCloseHandler = this.#handleTransportLoss.bind(this);
|
|
834
|
+
#transportErrorHandler = this.#handleTransportLoss.bind(this);
|
|
835
|
+
#processExitHandler = this.#handleProcessExit.bind(this);
|
|
756
836
|
constructor(options) {
|
|
757
837
|
this.#emitter = new Emitter({
|
|
758
838
|
...options?.on !== void 0 ? { on: options.on } : {},
|
|
@@ -779,11 +859,8 @@ var Browser = class {
|
|
|
779
859
|
get owned() {
|
|
780
860
|
return this.#owned;
|
|
781
861
|
}
|
|
782
|
-
get connected() {
|
|
783
|
-
return this.#status === "connected";
|
|
784
|
-
}
|
|
785
862
|
get pid() {
|
|
786
|
-
return this.#process?.pid;
|
|
863
|
+
return this.#servingPid ?? this.#process?.pid;
|
|
787
864
|
}
|
|
788
865
|
async discover() {
|
|
789
866
|
const result = await this.#discoverCDP();
|
|
@@ -792,19 +869,13 @@ var Browser = class {
|
|
|
792
869
|
}
|
|
793
870
|
async connect() {
|
|
794
871
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
795
|
-
const active = this.#connecting;
|
|
872
|
+
const active = this.#connecting.pending;
|
|
796
873
|
if (active !== void 0) {
|
|
797
874
|
await active;
|
|
798
875
|
return;
|
|
799
876
|
}
|
|
800
877
|
if (this.#status === "connected") return;
|
|
801
|
-
|
|
802
|
-
this.#connecting = attempt;
|
|
803
|
-
try {
|
|
804
|
-
await attempt;
|
|
805
|
-
} finally {
|
|
806
|
-
if (this.#connecting === attempt) this.#connecting = void 0;
|
|
807
|
-
}
|
|
878
|
+
await this.#connecting.execute(() => this.#establish());
|
|
808
879
|
}
|
|
809
880
|
adopt() {
|
|
810
881
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
@@ -813,19 +884,13 @@ var Browser = class {
|
|
|
813
884
|
}
|
|
814
885
|
async disconnect() {
|
|
815
886
|
if (this.#destroyed) return;
|
|
816
|
-
const disconnecting = this.#disconnecting;
|
|
817
|
-
const connecting = this.#connecting;
|
|
887
|
+
const disconnecting = this.#disconnecting.pending;
|
|
888
|
+
const connecting = this.#connecting.pending;
|
|
818
889
|
if (disconnecting !== void 0) await disconnecting;
|
|
819
890
|
if (connecting !== void 0) await connecting.catch(() => void 0);
|
|
820
891
|
if (this.#destroyed) return;
|
|
821
892
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
822
|
-
|
|
823
|
-
this.#disconnecting = attempt;
|
|
824
|
-
try {
|
|
825
|
-
await attempt;
|
|
826
|
-
} finally {
|
|
827
|
-
if (this.#disconnecting === attempt) this.#disconnecting = void 0;
|
|
828
|
-
}
|
|
893
|
+
await this.#disconnecting.execute(() => this.#detach());
|
|
829
894
|
}
|
|
830
895
|
context(index) {
|
|
831
896
|
const i = index ?? 0;
|
|
@@ -849,7 +914,7 @@ var Browser = class {
|
|
|
849
914
|
const result = await client.send("Target.createBrowserContext", params);
|
|
850
915
|
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
851
916
|
const id = result["browserContextId"];
|
|
852
|
-
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport,
|
|
917
|
+
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
|
|
853
918
|
try {
|
|
854
919
|
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
855
920
|
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
@@ -870,7 +935,7 @@ var Browser = class {
|
|
|
870
935
|
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
871
936
|
let context = this.#contexts[0];
|
|
872
937
|
if (context === void 0) {
|
|
873
|
-
context = new BrowserContext(client, void 0, this.#options.viewport,
|
|
938
|
+
context = new BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
|
|
874
939
|
this.#registerContext(context);
|
|
875
940
|
}
|
|
876
941
|
const page = await context.create(options);
|
|
@@ -898,7 +963,8 @@ var Browser = class {
|
|
|
898
963
|
return shutdown;
|
|
899
964
|
}
|
|
900
965
|
async #establish() {
|
|
901
|
-
|
|
966
|
+
const disconnecting = this.#disconnecting.pending;
|
|
967
|
+
if (disconnecting !== void 0) await disconnecting;
|
|
902
968
|
await this.#settleExit();
|
|
903
969
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
904
970
|
if (this.#status === "connected") return;
|
|
@@ -916,7 +982,7 @@ var Browser = class {
|
|
|
916
982
|
}
|
|
917
983
|
if (this.#options.cdp?.discover ?? true) {
|
|
918
984
|
const discovery = await this.#raceAbort(this.#discoverCDP(this.#signal()));
|
|
919
|
-
if (discovery.
|
|
985
|
+
if (discovery.endpoint !== void 0) {
|
|
920
986
|
this.#engine = browserToEngine(discovery.browser);
|
|
921
987
|
await this.#connectCDP(discovery.endpoint);
|
|
922
988
|
return;
|
|
@@ -979,6 +1045,7 @@ var Browser = class {
|
|
|
979
1045
|
this.#unbindTransport();
|
|
980
1046
|
this.#unbindProcess();
|
|
981
1047
|
this.#process = void 0;
|
|
1048
|
+
this.#servingPid = void 0;
|
|
982
1049
|
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
983
1050
|
this.#exitCleanup = cleanup;
|
|
984
1051
|
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
@@ -996,27 +1063,19 @@ var Browser = class {
|
|
|
996
1063
|
}
|
|
997
1064
|
#handleTransportLoss() {
|
|
998
1065
|
if (this.#destroyed || this.#status !== "connected") return;
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
this.#handleProcessExit();
|
|
1066
|
+
if (this.#process === void 0) {
|
|
1067
|
+
this.#confirmTransportLoss();
|
|
1002
1068
|
return;
|
|
1003
1069
|
}
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1070
|
+
if (!this.#alive()) {
|
|
1071
|
+
this.#handleProcessExit();
|
|
1006
1072
|
return;
|
|
1007
1073
|
}
|
|
1008
|
-
this.#confirmTransportLoss();
|
|
1074
|
+
setTimeout(() => this.#confirmTransportLoss(), 50);
|
|
1009
1075
|
}
|
|
1010
1076
|
#confirmTransportLoss() {
|
|
1011
1077
|
if (this.#destroyed || this.#status !== "connected") return;
|
|
1012
|
-
|
|
1013
|
-
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
1014
|
-
this.#handleProcessExit();
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (process?.pid !== void 0) try {
|
|
1018
|
-
process.kill(0);
|
|
1019
|
-
} catch {
|
|
1078
|
+
if (this.#process !== void 0 && !this.#alive()) {
|
|
1020
1079
|
this.#handleProcessExit();
|
|
1021
1080
|
return;
|
|
1022
1081
|
}
|
|
@@ -1041,23 +1100,24 @@ var Browser = class {
|
|
|
1041
1100
|
#bindTransport(transport) {
|
|
1042
1101
|
this.#unbindTransport();
|
|
1043
1102
|
this.#transport = transport;
|
|
1044
|
-
transport.emitter.on("close", this.#
|
|
1045
|
-
transport.emitter.on("error", this.#
|
|
1103
|
+
transport.emitter.on("close", this.#transportCloseHandler);
|
|
1104
|
+
transport.emitter.on("error", this.#transportErrorHandler);
|
|
1046
1105
|
}
|
|
1047
1106
|
#unbindTransport() {
|
|
1048
1107
|
const transport = this.#transport;
|
|
1049
1108
|
if (transport === void 0) return;
|
|
1050
|
-
transport.emitter.off("close", this.#
|
|
1051
|
-
transport.emitter.off("error", this.#
|
|
1109
|
+
transport.emitter.off("close", this.#transportCloseHandler);
|
|
1110
|
+
transport.emitter.off("error", this.#transportErrorHandler);
|
|
1052
1111
|
this.#transport = void 0;
|
|
1053
1112
|
}
|
|
1054
1113
|
#bindProcess(process) {
|
|
1055
1114
|
this.#unbindProcess();
|
|
1056
|
-
|
|
1057
|
-
|
|
1115
|
+
if (this.#servingPid !== void 0) return;
|
|
1116
|
+
process.once("exit", this.#processExitHandler);
|
|
1117
|
+
if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#processExitHandler);
|
|
1058
1118
|
}
|
|
1059
1119
|
#unbindProcess() {
|
|
1060
|
-
this.#process?.off("exit", this.#
|
|
1120
|
+
this.#process?.off("exit", this.#processExitHandler);
|
|
1061
1121
|
}
|
|
1062
1122
|
#timeout() {
|
|
1063
1123
|
return this.#options.timeout ?? BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -1070,13 +1130,9 @@ var Browser = class {
|
|
|
1070
1130
|
if (!response.ok) return this.#notFound();
|
|
1071
1131
|
const info = await response.json();
|
|
1072
1132
|
if (!isRecord(info)) return this.#notFound();
|
|
1073
|
-
const endpoint = isString(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0;
|
|
1074
|
-
const browser = isString(info["Browser"]) ? info["Browser"] : void 0;
|
|
1075
1133
|
return {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
browser,
|
|
1079
|
-
connection: endpoint !== void 0 ? "cdp" : void 0
|
|
1134
|
+
endpoint: isString(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0,
|
|
1135
|
+
browser: isString(info["Browser"]) ? info["Browser"] : void 0
|
|
1080
1136
|
};
|
|
1081
1137
|
} catch (error) {
|
|
1082
1138
|
if (signal?.aborted === true) throw error;
|
|
@@ -1085,10 +1141,8 @@ var Browser = class {
|
|
|
1085
1141
|
}
|
|
1086
1142
|
#notFound() {
|
|
1087
1143
|
return {
|
|
1088
|
-
found: false,
|
|
1089
1144
|
endpoint: void 0,
|
|
1090
|
-
browser: void 0
|
|
1091
|
-
connection: void 0
|
|
1145
|
+
browser: void 0
|
|
1092
1146
|
};
|
|
1093
1147
|
}
|
|
1094
1148
|
async #assertPortFree() {
|
|
@@ -1177,6 +1231,7 @@ var Browser = class {
|
|
|
1177
1231
|
timeout: this.#timeout()
|
|
1178
1232
|
});
|
|
1179
1233
|
await this.#raceAbort(client.connect());
|
|
1234
|
+
await this.#takeEndpointOwner(process, client);
|
|
1180
1235
|
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1181
1236
|
this.#client = client;
|
|
1182
1237
|
this.#endpoint = endpoint;
|
|
@@ -1195,12 +1250,49 @@ var Browser = class {
|
|
|
1195
1250
|
await this.#terminate(process);
|
|
1196
1251
|
await this.#releaseProfile();
|
|
1197
1252
|
this.#process = void 0;
|
|
1253
|
+
this.#servingPid = void 0;
|
|
1198
1254
|
this.#client = void 0;
|
|
1199
1255
|
this.#endpoint = void 0;
|
|
1200
1256
|
this.#owned = void 0;
|
|
1201
1257
|
throw error;
|
|
1202
1258
|
}
|
|
1203
1259
|
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Takes over the process serving the CDP endpoint when the spawned child is
|
|
1262
|
+
* no longer it.
|
|
1263
|
+
*
|
|
1264
|
+
* @remarks
|
|
1265
|
+
* A Windows launcher — Microsoft Edge — re-executes the browser with the
|
|
1266
|
+
* same `--remote-debugging-port` and exits 0 before the endpoint answers, so
|
|
1267
|
+
* the child this instance spawned parents the real tree without being part
|
|
1268
|
+
* of it. Chromium names the process behind the endpoint as the `browser`
|
|
1269
|
+
* entry of `SystemInfo.getProcessInfo`, which identifies it without a
|
|
1270
|
+
* platform branch. An endpoint that names no such process cannot be owned,
|
|
1271
|
+
* so the launch closes it and fails rather than leaking it.
|
|
1272
|
+
*/
|
|
1273
|
+
async #takeEndpointOwner(process, client) {
|
|
1274
|
+
if (process.exitCode === null && process.signalCode === null) return;
|
|
1275
|
+
let result;
|
|
1276
|
+
try {
|
|
1277
|
+
result = await client.send("SystemInfo.getProcessInfo");
|
|
1278
|
+
} catch {
|
|
1279
|
+
result = void 0;
|
|
1280
|
+
}
|
|
1281
|
+
let pid;
|
|
1282
|
+
if (isRecord(result) && isArray(result["processInfo"])) for (const entry of result["processInfo"]) {
|
|
1283
|
+
if (!isRecord(entry) || entry["type"] !== "browser" || !isInteger(entry["id"])) continue;
|
|
1284
|
+
pid = entry["id"];
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
if (pid === void 0) {
|
|
1288
|
+
await this.#closeRemote(client);
|
|
1289
|
+
throw new BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
|
|
1290
|
+
executable: this.#options.executable,
|
|
1291
|
+
pid: process.pid
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
this.#servingPid = pid;
|
|
1295
|
+
}
|
|
1204
1296
|
async #waitForLaunch(process, executable, args) {
|
|
1205
1297
|
const context = {
|
|
1206
1298
|
executable,
|
|
@@ -1212,6 +1304,7 @@ var Browser = class {
|
|
|
1212
1304
|
const exited = once(process, "exit", { signal }).then((values) => {
|
|
1213
1305
|
const code = isInteger(values[0]) ? values[0] : null;
|
|
1214
1306
|
const exitSignal = isString(values[1]) ? values[1] : null;
|
|
1307
|
+
if (code === 0 && exitSignal === null) return ready;
|
|
1215
1308
|
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1216
1309
|
});
|
|
1217
1310
|
try {
|
|
@@ -1250,13 +1343,13 @@ var Browser = class {
|
|
|
1250
1343
|
if (!isRecord(target) || !isString(target["targetId"]) || target["type"] !== "page" || !isString(target["title"]) || !isString(target["url"])) continue;
|
|
1251
1344
|
pages.push({
|
|
1252
1345
|
id: target["targetId"],
|
|
1253
|
-
|
|
1346
|
+
category: target["type"],
|
|
1254
1347
|
title: target["title"],
|
|
1255
1348
|
url: target["url"]
|
|
1256
1349
|
});
|
|
1257
1350
|
}
|
|
1258
1351
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
1259
|
-
const context = new BrowserContext(client, void 0, this.#options.viewport,
|
|
1352
|
+
const context = new BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
|
|
1260
1353
|
await context.sync(pages);
|
|
1261
1354
|
this.#registerContext(context);
|
|
1262
1355
|
}
|
|
@@ -1274,6 +1367,7 @@ var Browser = class {
|
|
|
1274
1367
|
this.#contexts = [];
|
|
1275
1368
|
await this.#terminate(this.#process);
|
|
1276
1369
|
this.#process = void 0;
|
|
1370
|
+
this.#servingPid = void 0;
|
|
1277
1371
|
terminated = true;
|
|
1278
1372
|
} finally {
|
|
1279
1373
|
const contexts = this.#contexts;
|
|
@@ -1303,6 +1397,7 @@ var Browser = class {
|
|
|
1303
1397
|
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1304
1398
|
}
|
|
1305
1399
|
this.#process = void 0;
|
|
1400
|
+
this.#servingPid = void 0;
|
|
1306
1401
|
terminated = true;
|
|
1307
1402
|
} finally {
|
|
1308
1403
|
const contexts = this.#contexts;
|
|
@@ -1318,8 +1413,8 @@ var Browser = class {
|
|
|
1318
1413
|
}
|
|
1319
1414
|
}
|
|
1320
1415
|
async #settle() {
|
|
1321
|
-
await this.#connecting?.catch(() => void 0);
|
|
1322
|
-
await this.#disconnecting?.catch(() => void 0);
|
|
1416
|
+
await this.#connecting.pending?.catch(() => void 0);
|
|
1417
|
+
await this.#disconnecting.pending?.catch(() => void 0);
|
|
1323
1418
|
await this.#settleExit();
|
|
1324
1419
|
}
|
|
1325
1420
|
async #settleExit() {
|
|
@@ -1414,31 +1509,39 @@ var Browser = class {
|
|
|
1414
1509
|
const pid = process.pid;
|
|
1415
1510
|
return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
|
|
1416
1511
|
}
|
|
1417
|
-
#
|
|
1418
|
-
|
|
1512
|
+
#alive() {
|
|
1513
|
+
const process = this.#process;
|
|
1514
|
+
if (process === void 0) return false;
|
|
1515
|
+
if (this.#servingPid !== void 0) return this.#signalProcess(process, 0) !== false;
|
|
1516
|
+
return process.exitCode === null && process.signalCode === null;
|
|
1517
|
+
}
|
|
1518
|
+
#inspectRemainder(process) {
|
|
1519
|
+
if (!this.#hasProcessGroup(process) && this.#servingPid === void 0) return false;
|
|
1419
1520
|
return this.#signalProcess(process, 0);
|
|
1420
1521
|
}
|
|
1421
1522
|
#signalProcess(process, signal) {
|
|
1422
1523
|
const pid = process.pid;
|
|
1524
|
+
const serving = this.#servingPid;
|
|
1423
1525
|
try {
|
|
1424
1526
|
if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
|
|
1527
|
+
if (serving !== void 0) return globalThis.process.kill(serving, signal);
|
|
1425
1528
|
return process.kill(signal);
|
|
1426
1529
|
} catch (error) {
|
|
1427
1530
|
const code = isError(error) && "code" in error && isString(error.code) ? error.code : void 0;
|
|
1428
1531
|
if (code === "ESRCH") return false;
|
|
1429
1532
|
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
1430
1533
|
throw new BrowserConnectionError("Failed to signal the browser process", {
|
|
1431
|
-
pid,
|
|
1534
|
+
pid: serving ?? pid,
|
|
1432
1535
|
signal,
|
|
1433
1536
|
cause: error
|
|
1434
1537
|
});
|
|
1435
1538
|
}
|
|
1436
1539
|
}
|
|
1437
|
-
async #
|
|
1540
|
+
async #waitForRemainderWithin(process, timeout) {
|
|
1438
1541
|
const deadline = performance.now() + timeout;
|
|
1439
1542
|
let confirmed = false;
|
|
1440
1543
|
while (true) {
|
|
1441
|
-
const alive = this.#
|
|
1544
|
+
const alive = this.#inspectRemainder(process);
|
|
1442
1545
|
if (alive === false) return true;
|
|
1443
1546
|
if (alive === true) confirmed = true;
|
|
1444
1547
|
const remaining = deadline - performance.now();
|
|
@@ -1448,17 +1551,17 @@ var Browser = class {
|
|
|
1448
1551
|
}
|
|
1449
1552
|
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
1450
1553
|
const exited = this.#waitForProcessWithin(process, timeout);
|
|
1451
|
-
const drained = this.#
|
|
1554
|
+
const drained = this.#waitForRemainderWithin(process, timeout);
|
|
1452
1555
|
const results = await Promise.all([exited, drained]);
|
|
1453
1556
|
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
1454
1557
|
}
|
|
1455
1558
|
async #terminate(process) {
|
|
1456
|
-
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#
|
|
1559
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectRemainder(process) === false) return;
|
|
1457
1560
|
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
1458
1561
|
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
1459
1562
|
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
1460
1563
|
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
1461
|
-
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
|
|
1564
|
+
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
|
|
1462
1565
|
}
|
|
1463
1566
|
#finish() {
|
|
1464
1567
|
this.#unbindTransport();
|
|
@@ -1469,6 +1572,7 @@ var Browser = class {
|
|
|
1469
1572
|
this.#endpoint = void 0;
|
|
1470
1573
|
this.#client = void 0;
|
|
1471
1574
|
this.#process = void 0;
|
|
1575
|
+
this.#servingPid = void 0;
|
|
1472
1576
|
this.#exitCleanup = void 0;
|
|
1473
1577
|
this.#contexts = [];
|
|
1474
1578
|
this.#emitter.emit("destroy");
|
|
@@ -1476,6 +1580,6 @@ var Browser = class {
|
|
|
1476
1580
|
}
|
|
1477
1581
|
};
|
|
1478
1582
|
//#endregion
|
|
1479
|
-
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_HEADLESS_ARG, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserConnectionError, BrowserDestroyedError, BrowserNotConnectedError, WebSocketCDPTransport, browserToEngine,
|
|
1583
|
+
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_HEADLESS_ARG, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserConnectionError, BrowserDestroyedError, BrowserNotConnectedError, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserProfile, createBrowserWriter, createCDPTransport, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, isBrowserConnectionError, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, probePathNames, readFirstLine, removeBrowserProfile, waitForCDPReady };
|
|
1480
1584
|
|
|
1481
1585
|
//# sourceMappingURL=index.js.map
|