@orkestrel/browser 0.0.3 → 0.0.6
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 +12 -3
- package/dist/src/core/index.cjs +6824 -637
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2913 -307
- package/dist/src/core/index.d.ts +2913 -307
- package/dist/src/core/index.js +6674 -639
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +708 -418
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +95 -53
- package/dist/src/server/index.d.ts +95 -53
- package/dist/src/server/index.js +705 -419
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -6
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _src_core = require("../core/index.cjs");
|
|
3
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
4
|
let node_fs = require("node:fs");
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
6
|
+
let node_os = require("node:os");
|
|
4
7
|
let node_path = require("node:path");
|
|
5
8
|
let node_child_process = require("node:child_process");
|
|
6
|
-
let
|
|
9
|
+
let node_timers_promises = require("node:timers/promises");
|
|
7
10
|
let node_crypto = require("node:crypto");
|
|
8
11
|
let node_http = require("node:http");
|
|
12
|
+
let node_https = require("node:https");
|
|
9
13
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
10
14
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
11
|
-
let
|
|
15
|
+
let node_events = require("node:events");
|
|
12
16
|
//#region src/server/constants.ts
|
|
13
17
|
/** Default CDP port probed for an existing browser and used for launches. */
|
|
14
18
|
var BROWSER_DEFAULT_CDP_PORT = 9222;
|
|
@@ -20,23 +24,22 @@ var BROWSER_CDP_PROTOCOL = "http";
|
|
|
20
24
|
var BROWSER_CDP_VERSION_PATH = "/json/version";
|
|
21
25
|
/** Path appended to the CDP host to list open targets (pages, workers, etc). */
|
|
22
26
|
var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
23
|
-
/** Sentinel result returned by discovery when no browser is reachable. */
|
|
24
|
-
var BROWSER_NOT_FOUND_RESULT = Object.freeze({
|
|
25
|
-
found: false,
|
|
26
|
-
endpoint: void 0,
|
|
27
|
-
browser: void 0,
|
|
28
|
-
connection: void 0
|
|
29
|
-
});
|
|
30
27
|
/** Flags always passed to a launched browser process, alongside the caller's own. */
|
|
31
28
|
var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
|
|
32
29
|
/** Flag enabling headless mode on a launched browser process. */
|
|
33
30
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
31
|
+
/** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
32
|
+
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
34
33
|
/** Grace period after SIGTERM before a launched process is escalated to SIGKILL during teardown. */
|
|
35
34
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
36
35
|
/** Bound for the `discover: false` port-occupancy probe before launching — short, since it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
|
|
37
36
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
38
37
|
/** Brief defer applied once 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. */
|
|
39
38
|
var BROWSER_TRANSPORT_LOSS_DEFER_MS = 50;
|
|
39
|
+
/** Machine-readable error-context cause for an owned browser process exiting. */
|
|
40
|
+
var BROWSER_PROCESS_EXIT_CAUSE = "process-exit";
|
|
41
|
+
/** Machine-readable error-context cause for a CDP transport disconnecting while its browser remains alive. */
|
|
42
|
+
var BROWSER_TRANSPORT_LOSS_CAUSE = "transport-loss";
|
|
40
43
|
/** Environment variables checked (in order) for an explicit browser executable path override. */
|
|
41
44
|
var BROWSER_ENV_PATH_KEYS = Object.freeze(["PLAYWRIGHT_EXECUTABLE_PATH", "CHROME_PATH"]);
|
|
42
45
|
/** Well-known Chrome/Chromium/Edge executable paths with no platform-specific root, keyed by `process.platform`. */
|
|
@@ -158,7 +161,7 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
|
|
|
158
161
|
* @returns True when value is a BrowserConnectionError instance
|
|
159
162
|
*/
|
|
160
163
|
function isBrowserConnectionError(value) {
|
|
161
|
-
return value
|
|
164
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
|
|
162
165
|
}
|
|
163
166
|
/**
|
|
164
167
|
* Narrow an unknown value to BrowserNotConnectedError.
|
|
@@ -167,7 +170,7 @@ function isBrowserConnectionError(value) {
|
|
|
167
170
|
* @returns True when value is a BrowserNotConnectedError instance
|
|
168
171
|
*/
|
|
169
172
|
function isBrowserNotConnectedError(value) {
|
|
170
|
-
return value
|
|
173
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
|
|
171
174
|
}
|
|
172
175
|
/**
|
|
173
176
|
* Narrow an unknown value to BrowserDestroyedError.
|
|
@@ -176,7 +179,7 @@ function isBrowserNotConnectedError(value) {
|
|
|
176
179
|
* @returns True when value is a BrowserDestroyedError instance
|
|
177
180
|
*/
|
|
178
181
|
function isBrowserDestroyedError(value) {
|
|
179
|
-
return value
|
|
182
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
|
|
180
183
|
}
|
|
181
184
|
//#endregion
|
|
182
185
|
//#region src/server/helpers.ts
|
|
@@ -260,10 +263,48 @@ function normalizeExecutablePath(path, platform) {
|
|
|
260
263
|
}
|
|
261
264
|
/** Classify a `/json/version` `Browser` string into a {@link BrowserEngine} (`Edg/` → edge, `Chrome/` → chrome, else chromium). */
|
|
262
265
|
function browserToEngine(browser) {
|
|
263
|
-
if (browser
|
|
264
|
-
if (browser
|
|
266
|
+
if (browser?.includes("Edg/") === true) return "edge";
|
|
267
|
+
if (browser?.includes("Chrome/") === true) return "chrome";
|
|
265
268
|
return "chromium";
|
|
266
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a persistent caller profile or create an isolated temporary one.
|
|
272
|
+
*
|
|
273
|
+
* @param path - Optional caller-owned persistent user-data directory
|
|
274
|
+
* @returns The resolved profile and whether the library owns its lifecycle
|
|
275
|
+
*/
|
|
276
|
+
async function createBrowserProfile(path) {
|
|
277
|
+
if (path !== void 0) return {
|
|
278
|
+
path,
|
|
279
|
+
temporary: false
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
path: await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), BROWSER_PROFILE_PREFIX)),
|
|
283
|
+
temporary: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Remove a library-owned isolated browser profile.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* The path must remain a direct child of the operating-system temp directory
|
|
291
|
+
* with {@link BROWSER_PROFILE_PREFIX}; this defensive check prevents a
|
|
292
|
+
* malformed profile value from widening recursive deletion.
|
|
293
|
+
*
|
|
294
|
+
* @param profile - Resolved profile lifecycle result
|
|
295
|
+
*/
|
|
296
|
+
async function removeBrowserProfile(profile) {
|
|
297
|
+
if (!profile.temporary) return;
|
|
298
|
+
const path = (0, node_path.resolve)(profile.path);
|
|
299
|
+
const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
|
|
300
|
+
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new Error("Refusing to remove an unsafe browser profile path");
|
|
301
|
+
await (0, node_fs_promises.rm)(path, {
|
|
302
|
+
recursive: true,
|
|
303
|
+
force: true,
|
|
304
|
+
maxRetries: 3,
|
|
305
|
+
retryDelay: 100
|
|
306
|
+
});
|
|
307
|
+
}
|
|
267
308
|
/** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
|
|
268
309
|
function findEnvOverride(env) {
|
|
269
310
|
return findAllEnvOverrides(env)[0];
|
|
@@ -347,7 +388,7 @@ function findAllInStore(base, platform) {
|
|
|
347
388
|
const glob = BROWSER_STORE_GLOBS[platform];
|
|
348
389
|
if (glob === void 0) return found;
|
|
349
390
|
const matches = (0, node_fs.globSync)(`${base.replaceAll("\\", "/")}/${glob}`).filter((match) => (0, node_fs.existsSync)(match));
|
|
350
|
-
matches.sort((a, b) => a
|
|
391
|
+
matches.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
351
392
|
found.push(...matches);
|
|
352
393
|
return found;
|
|
353
394
|
}
|
|
@@ -379,23 +420,24 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
379
420
|
*
|
|
380
421
|
* @throws When the endpoint does not become ready before the timeout
|
|
381
422
|
*/
|
|
382
|
-
async function
|
|
423
|
+
async function waitForCDPReady(port, timeout, host = BROWSER_DEFAULT_HOST, signal) {
|
|
383
424
|
const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_VERSION_PATH}`;
|
|
384
425
|
const deadline = Date.now() + timeout;
|
|
385
426
|
while (Date.now() < deadline) {
|
|
427
|
+
signal?.throwIfAborted();
|
|
386
428
|
const remaining = Math.max(0, deadline - Date.now());
|
|
387
|
-
const
|
|
388
|
-
const timer = setTimeout(() => controller.abort(), remaining);
|
|
429
|
+
const requestSignal = signal === void 0 ? AbortSignal.timeout(remaining) : AbortSignal.any([signal, AbortSignal.timeout(remaining)]);
|
|
389
430
|
try {
|
|
390
|
-
const response = await fetch(url, { signal:
|
|
431
|
+
const response = await fetch(url, { signal: requestSignal });
|
|
391
432
|
if (response.ok) {
|
|
392
433
|
const info = await response.json();
|
|
393
434
|
if ((0, _orkestrel_contract.isRecord)(info) && (0, _orkestrel_contract.isString)(info["webSocketDebuggerUrl"])) return info["webSocketDebuggerUrl"];
|
|
394
435
|
}
|
|
395
|
-
} catch
|
|
396
|
-
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (signal?.aborted === true) throw error;
|
|
397
438
|
}
|
|
398
|
-
|
|
439
|
+
const delay = Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
440
|
+
if (delay > 0) await (0, node_timers_promises.setTimeout)(delay, void 0, { signal });
|
|
399
441
|
}
|
|
400
442
|
throw new Error(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`);
|
|
401
443
|
}
|
|
@@ -407,7 +449,7 @@ async function waitForCdpReady(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
407
449
|
* @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
|
|
408
450
|
* @returns Normalized CDP targets
|
|
409
451
|
*/
|
|
410
|
-
async function
|
|
452
|
+
async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
411
453
|
const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_LIST_PATH}`;
|
|
412
454
|
const controller = new AbortController();
|
|
413
455
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
@@ -415,16 +457,16 @@ async function fetchCdpTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
415
457
|
const response = await fetch(url, { signal: controller.signal });
|
|
416
458
|
if (!response.ok) return [];
|
|
417
459
|
const list = await response.json();
|
|
418
|
-
if (!
|
|
460
|
+
if (!(0, _orkestrel_contract.isArray)(list)) return [];
|
|
419
461
|
const targets = [];
|
|
420
462
|
for (const entry of list) {
|
|
421
463
|
if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
|
|
422
|
-
if (!(0, _orkestrel_contract.isString)(entry["id"]) || !(0, _orkestrel_contract.isString)(entry["type"])) continue;
|
|
464
|
+
if (!(0, _orkestrel_contract.isString)(entry["id"]) || !(0, _orkestrel_contract.isString)(entry["type"]) || !(0, _orkestrel_contract.isString)(entry["title"]) || !(0, _orkestrel_contract.isString)(entry["url"])) continue;
|
|
423
465
|
targets.push({
|
|
424
466
|
id: entry["id"],
|
|
425
467
|
type: entry["type"],
|
|
426
|
-
title:
|
|
427
|
-
url:
|
|
468
|
+
title: entry["title"],
|
|
469
|
+
url: entry["url"]
|
|
428
470
|
});
|
|
429
471
|
}
|
|
430
472
|
return targets;
|
|
@@ -437,33 +479,32 @@ async function fetchCdpTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
437
479
|
//#endregion
|
|
438
480
|
//#region src/server/transports/WebSocketCDPTransport.ts
|
|
439
481
|
/**
|
|
440
|
-
* `@orkestrel/websocket`-backed
|
|
441
|
-
* pipe a {@link import('@src/core').CDPClientInterface} sends and receives
|
|
442
|
-
* JSON-RPC frames over.
|
|
482
|
+
* `@orkestrel/websocket`-backed raw CDP text transport.
|
|
443
483
|
*
|
|
444
484
|
* @remarks
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* `
|
|
448
|
-
*
|
|
449
|
-
* — a mismatch (or a connection/request error, or a timeout) rejects `start()`
|
|
450
|
-
* with a coded {@link BrowserConnectionError} and cleans up the socket. On
|
|
451
|
-
* success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
452
|
-
* head })` (CLIENT mode — no `key` — so outgoing frames are masked per RFC
|
|
453
|
-
* 6455 §5.3) and bridges its `message`/`close`/`error` events onto this
|
|
454
|
-
* transport's emitter. `start()` opens a fresh socket each call (safe to call
|
|
455
|
-
* again after `close()` — a new request/socket/NodeWebSocket triple is
|
|
456
|
-
* created every time, which is what `CDPClient.reconnect()` depends on). CDP
|
|
457
|
-
* frames are text-only, so `message` always carries a `string`.
|
|
485
|
+
* Performs and validates the RFC 6455 HTTP upgrade, then delegates frame
|
|
486
|
+
* handling to `@orkestrel/websocket`. Concurrent and repeated `start()` /
|
|
487
|
+
* `close()` calls share their active transition, and a later `start()` opens a
|
|
488
|
+
* fresh socket after the prior one closes.
|
|
458
489
|
*/
|
|
459
490
|
var WebSocketCDPTransport = class {
|
|
460
491
|
#emitter;
|
|
461
492
|
#url;
|
|
462
493
|
#timeout;
|
|
463
494
|
#socket;
|
|
464
|
-
#
|
|
495
|
+
#starting;
|
|
496
|
+
#closing;
|
|
497
|
+
#request;
|
|
498
|
+
#resolve;
|
|
499
|
+
#reject;
|
|
500
|
+
#timer;
|
|
501
|
+
#closeResolve;
|
|
502
|
+
#onSocketClose = () => this.#closeResolve?.();
|
|
465
503
|
constructor(options) {
|
|
466
|
-
this.#emitter = new _orkestrel_emitter.Emitter(
|
|
504
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
505
|
+
on: options.on,
|
|
506
|
+
error: options.error
|
|
507
|
+
});
|
|
467
508
|
this.#url = options.url;
|
|
468
509
|
this.#timeout = options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
|
|
469
510
|
}
|
|
@@ -471,62 +512,22 @@ var WebSocketCDPTransport = class {
|
|
|
471
512
|
return this.#emitter;
|
|
472
513
|
}
|
|
473
514
|
async start() {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
port: url.port.length > 0 ? Number(url.port) : 80,
|
|
491
|
-
path: `${url.pathname}${url.search}`,
|
|
492
|
-
headers: {
|
|
493
|
-
Connection: "Upgrade",
|
|
494
|
-
Upgrade: "websocket",
|
|
495
|
-
"Sec-WebSocket-Key": key,
|
|
496
|
-
"Sec-WebSocket-Version": _orkestrel_websocket.WEBSOCKET_VERSION
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
request.on("upgrade", (response, socket, head) => {
|
|
500
|
-
clearTimeout(timer);
|
|
501
|
-
const accept = response.headers["sec-websocket-accept"];
|
|
502
|
-
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
503
|
-
socket.destroy();
|
|
504
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url })));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
settle(() => resolve((0, _orkestrel_websocket.createNodeWebSocket)({
|
|
508
|
-
socket,
|
|
509
|
-
head
|
|
510
|
-
})));
|
|
511
|
-
});
|
|
512
|
-
request.on("response", (response) => {
|
|
513
|
-
clearTimeout(timer);
|
|
514
|
-
response.resume();
|
|
515
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: upgrade declined with status ${response.statusCode ?? 0}`, { url: this.#url })));
|
|
516
|
-
});
|
|
517
|
-
request.on("error", (error) => {
|
|
518
|
-
clearTimeout(timer);
|
|
519
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url })));
|
|
520
|
-
});
|
|
521
|
-
this.#connecting = {
|
|
522
|
-
abort: () => request.destroy(),
|
|
523
|
-
timer,
|
|
524
|
-
reject: (error) => settle(() => reject(error))
|
|
525
|
-
};
|
|
526
|
-
request.end();
|
|
527
|
-
});
|
|
528
|
-
this.#socket = ws;
|
|
529
|
-
this.#bind(ws);
|
|
515
|
+
if (this.#closing !== void 0) await this.#closing;
|
|
516
|
+
if (this.#socket?.readyState === _orkestrel_websocket.WEBSOCKET_READY_OPEN) return;
|
|
517
|
+
const active = this.#starting;
|
|
518
|
+
if (active !== void 0) {
|
|
519
|
+
await active;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
this.#socket?.destroy();
|
|
523
|
+
this.#socket = void 0;
|
|
524
|
+
const transition = this.#open();
|
|
525
|
+
this.#starting = transition;
|
|
526
|
+
try {
|
|
527
|
+
await transition;
|
|
528
|
+
} finally {
|
|
529
|
+
if (this.#starting === transition) this.#starting = void 0;
|
|
530
|
+
}
|
|
530
531
|
}
|
|
531
532
|
async send(data) {
|
|
532
533
|
const socket = this.#socket;
|
|
@@ -534,32 +535,144 @@ var WebSocketCDPTransport = class {
|
|
|
534
535
|
socket.send(data);
|
|
535
536
|
}
|
|
536
537
|
async close() {
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
this.#connecting = void 0;
|
|
541
|
-
connecting.abort();
|
|
542
|
-
connecting.reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
538
|
+
const active = this.#closing;
|
|
539
|
+
if (active !== void 0) {
|
|
540
|
+
await active;
|
|
543
541
|
return;
|
|
544
542
|
}
|
|
543
|
+
const transition = this.#stop();
|
|
544
|
+
this.#closing = transition;
|
|
545
|
+
try {
|
|
546
|
+
await transition;
|
|
547
|
+
} finally {
|
|
548
|
+
if (this.#closing === transition) this.#closing = void 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async #open() {
|
|
552
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
|
|
553
|
+
if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
554
|
+
url: this.#url,
|
|
555
|
+
error: parsed.error
|
|
556
|
+
});
|
|
557
|
+
const url = parsed.value;
|
|
558
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
559
|
+
const key = (0, node_crypto.randomBytes)(16).toString("base64");
|
|
560
|
+
const deferred = Promise.withResolvers();
|
|
561
|
+
const options = {
|
|
562
|
+
hostname: url.hostname,
|
|
563
|
+
port: url.port.length > 0 ? Number(url.port) : url.protocol === "wss:" ? 443 : 80,
|
|
564
|
+
path: `${url.pathname}${url.search}`,
|
|
565
|
+
headers: {
|
|
566
|
+
Connection: "Upgrade",
|
|
567
|
+
Upgrade: "websocket",
|
|
568
|
+
"Sec-WebSocket-Key": key,
|
|
569
|
+
"Sec-WebSocket-Version": _orkestrel_websocket.WEBSOCKET_VERSION
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const request = url.protocol === "wss:" ? (0, node_https.request)(options) : (0, node_http.request)(options);
|
|
573
|
+
this.#request = request;
|
|
574
|
+
this.#resolve = deferred.resolve;
|
|
575
|
+
this.#reject = deferred.reject;
|
|
576
|
+
this.#timer = setTimeout(() => this.#expire(request), this.#timeout);
|
|
577
|
+
request.on("upgrade", (response, socket, head) => {
|
|
578
|
+
this.#upgrade(request, key, response, socket, head);
|
|
579
|
+
});
|
|
580
|
+
request.on("response", (response) => this.#decline(request, response));
|
|
581
|
+
request.on("error", (error) => this.#fail(request, error));
|
|
582
|
+
request.end();
|
|
583
|
+
const socket = await deferred.promise;
|
|
584
|
+
this.#socket = socket;
|
|
585
|
+
this.#bind(socket);
|
|
586
|
+
}
|
|
587
|
+
async #stop() {
|
|
588
|
+
const request = this.#request;
|
|
589
|
+
if (request !== void 0) {
|
|
590
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
591
|
+
request.destroy();
|
|
592
|
+
}
|
|
593
|
+
await this.#starting?.catch(() => void 0);
|
|
545
594
|
const socket = this.#socket;
|
|
546
|
-
if (socket === void 0) return;
|
|
547
595
|
this.#socket = void 0;
|
|
548
|
-
|
|
549
|
-
|
|
596
|
+
if (socket === void 0 || socket.readyState === _orkestrel_websocket.WEBSOCKET_READY_CLOSED) return;
|
|
597
|
+
const closed = Promise.withResolvers();
|
|
598
|
+
this.#closeResolve = closed.resolve;
|
|
599
|
+
socket.emitter.on("close", this.#onSocketClose);
|
|
600
|
+
try {
|
|
550
601
|
socket.close();
|
|
551
|
-
|
|
602
|
+
if (socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
603
|
+
} catch {
|
|
604
|
+
socket.destroy();
|
|
605
|
+
if (socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
606
|
+
} finally {
|
|
607
|
+
socket.emitter.off("close", this.#onSocketClose);
|
|
608
|
+
this.#closeResolve = void 0;
|
|
609
|
+
}
|
|
552
610
|
}
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
611
|
+
#upgrade(request, key, response, socket, head) {
|
|
612
|
+
if (this.#request !== request) {
|
|
613
|
+
socket.destroy();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const accept = response.headers["sec-websocket-accept"];
|
|
617
|
+
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
618
|
+
socket.destroy();
|
|
619
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
this.#resolveRequest(request, (0, _orkestrel_websocket.createNodeWebSocket)({
|
|
624
|
+
socket,
|
|
625
|
+
head
|
|
626
|
+
}));
|
|
627
|
+
} catch (error) {
|
|
628
|
+
socket.destroy();
|
|
629
|
+
this.#rejectRequest(request, error);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
#decline(request, response) {
|
|
633
|
+
response.resume();
|
|
634
|
+
const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
|
|
635
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
|
|
636
|
+
}
|
|
637
|
+
#fail(request, error) {
|
|
638
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
|
|
639
|
+
}
|
|
640
|
+
#expire(request) {
|
|
641
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
|
|
642
|
+
url: this.#url,
|
|
643
|
+
timeout: this.#timeout
|
|
644
|
+
}));
|
|
645
|
+
request.destroy();
|
|
646
|
+
}
|
|
647
|
+
#resolveRequest(request, socket) {
|
|
648
|
+
if (this.#request !== request) {
|
|
649
|
+
socket.destroy();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const resolve = this.#resolve;
|
|
653
|
+
this.#clearRequest();
|
|
654
|
+
resolve?.(socket);
|
|
655
|
+
}
|
|
656
|
+
#rejectRequest(request, error) {
|
|
657
|
+
if (this.#request !== request) return;
|
|
658
|
+
const reject = this.#reject;
|
|
659
|
+
this.#clearRequest();
|
|
660
|
+
reject?.(error);
|
|
661
|
+
}
|
|
662
|
+
#clearRequest() {
|
|
663
|
+
if (this.#timer !== void 0) clearTimeout(this.#timer);
|
|
664
|
+
this.#timer = void 0;
|
|
665
|
+
this.#request = void 0;
|
|
666
|
+
this.#resolve = void 0;
|
|
667
|
+
this.#reject = void 0;
|
|
668
|
+
}
|
|
669
|
+
#bind(socket) {
|
|
670
|
+
socket.emitter.on("message", (data) => this.#emitter.emit("message", data));
|
|
671
|
+
socket.emitter.on("close", () => {
|
|
672
|
+
if (this.#socket === socket) this.#socket = void 0;
|
|
558
673
|
this.#emitter.emit("close");
|
|
559
674
|
});
|
|
560
|
-
|
|
561
|
-
this.#emitter.emit("error", error);
|
|
562
|
-
});
|
|
675
|
+
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
563
676
|
}
|
|
564
677
|
};
|
|
565
678
|
//#endregion
|
|
@@ -603,23 +716,37 @@ function createScreenshotWriter() {
|
|
|
603
716
|
}
|
|
604
717
|
//#endregion
|
|
605
718
|
//#region src/server/Browser.ts
|
|
719
|
+
/**
|
|
720
|
+
* Discovers, launches, connects to, and owns Chromium-family browser sessions.
|
|
721
|
+
*/
|
|
606
722
|
var Browser = class {
|
|
607
723
|
#emitter;
|
|
608
724
|
#options;
|
|
725
|
+
#abort = new AbortController();
|
|
726
|
+
#cdpPort;
|
|
727
|
+
#cdpHost;
|
|
609
728
|
#engine;
|
|
610
729
|
#status = "idle";
|
|
611
730
|
#connection;
|
|
731
|
+
#owned;
|
|
732
|
+
#endpoint;
|
|
612
733
|
#client;
|
|
613
734
|
#process;
|
|
614
|
-
#
|
|
615
|
-
#cdpHost;
|
|
735
|
+
#profile;
|
|
616
736
|
#contexts = [];
|
|
617
737
|
#destroyed = false;
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
#
|
|
738
|
+
#connecting;
|
|
739
|
+
#disconnecting;
|
|
740
|
+
#shutdown;
|
|
741
|
+
#transport;
|
|
742
|
+
#onTransportClose = () => this.#handleTransportLoss();
|
|
743
|
+
#onTransportError = () => this.#handleTransportLoss();
|
|
744
|
+
#onProcessExit = () => this.#handleProcessExit();
|
|
621
745
|
constructor(options) {
|
|
622
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
746
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
747
|
+
on: options?.on,
|
|
748
|
+
error: options?.error
|
|
749
|
+
});
|
|
623
750
|
this.#options = options ?? {};
|
|
624
751
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
625
752
|
this.#cdpPort = this.#options.cdp?.port ?? 9222;
|
|
@@ -638,63 +765,62 @@ var Browser = class {
|
|
|
638
765
|
get connection() {
|
|
639
766
|
return this.#connection;
|
|
640
767
|
}
|
|
768
|
+
get owned() {
|
|
769
|
+
return this.#owned;
|
|
770
|
+
}
|
|
641
771
|
get connected() {
|
|
642
772
|
return this.#status === "connected";
|
|
643
773
|
}
|
|
644
774
|
get pid() {
|
|
645
|
-
return this.#process?.pid
|
|
775
|
+
return this.#process?.pid;
|
|
646
776
|
}
|
|
647
777
|
async discover() {
|
|
648
|
-
const result = await this.#
|
|
778
|
+
const result = await this.#discoverCDP();
|
|
649
779
|
this.#emitter.emit("discover", result);
|
|
650
780
|
return result;
|
|
651
781
|
}
|
|
652
782
|
async connect() {
|
|
783
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
784
|
+
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
653
785
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
654
786
|
if (this.#status === "connected") return;
|
|
655
|
-
this.#
|
|
656
|
-
|
|
787
|
+
const active = this.#connecting;
|
|
788
|
+
if (active !== void 0) {
|
|
789
|
+
await active;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const attempt = this.#establish();
|
|
793
|
+
this.#connecting = attempt;
|
|
657
794
|
try {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (this.#options.cdp?.discover ?? true) {
|
|
664
|
-
this.#assertNotAborted();
|
|
665
|
-
const discovery = await this.#raceAbort(this.#discoverCdp());
|
|
666
|
-
if (discovery.found && discovery.endpoint !== void 0) {
|
|
667
|
-
this.#engine = browserToEngine(discovery.browser ?? "");
|
|
668
|
-
await this.#connectCdp(discovery.endpoint);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
} else {
|
|
672
|
-
this.#assertNotAborted();
|
|
673
|
-
await this.#raceAbort(this.#assertPortFree());
|
|
674
|
-
}
|
|
675
|
-
this.#assertNotAborted();
|
|
676
|
-
await this.#launch();
|
|
677
|
-
} catch (thrown) {
|
|
678
|
-
this.#status = "error";
|
|
679
|
-
this.#emitter.emit("error", thrown);
|
|
680
|
-
if (thrown instanceof BrowserConnectionError) throw thrown;
|
|
681
|
-
throw new BrowserConnectionError(thrown instanceof Error ? thrown.message : String(thrown), { executable: this.#options.executable });
|
|
795
|
+
await attempt;
|
|
796
|
+
} finally {
|
|
797
|
+
if (this.#connecting === attempt) this.#connecting = void 0;
|
|
682
798
|
}
|
|
683
799
|
}
|
|
800
|
+
adopt() {
|
|
801
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
802
|
+
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new BrowserNotConnectedError();
|
|
803
|
+
this.#owned = true;
|
|
804
|
+
}
|
|
684
805
|
async disconnect() {
|
|
685
|
-
if (this.#destroyed
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.#
|
|
696
|
-
this.#
|
|
697
|
-
this.#
|
|
806
|
+
if (this.#destroyed) return;
|
|
807
|
+
const active = this.#disconnecting;
|
|
808
|
+
if (active !== void 0) {
|
|
809
|
+
await active;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (this.#connecting !== void 0) {
|
|
813
|
+
await this.#connecting.catch(() => void 0);
|
|
814
|
+
if (this.#destroyed) return;
|
|
815
|
+
}
|
|
816
|
+
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
817
|
+
const attempt = this.#detach();
|
|
818
|
+
this.#disconnecting = attempt;
|
|
819
|
+
try {
|
|
820
|
+
await attempt;
|
|
821
|
+
} finally {
|
|
822
|
+
if (this.#disconnecting === attempt) this.#disconnecting = void 0;
|
|
823
|
+
}
|
|
698
824
|
}
|
|
699
825
|
context(index) {
|
|
700
826
|
const i = index ?? 0;
|
|
@@ -704,239 +830,250 @@ var Browser = class {
|
|
|
704
830
|
contexts() {
|
|
705
831
|
return [...this.#contexts];
|
|
706
832
|
}
|
|
833
|
+
async isolate(options) {
|
|
834
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
835
|
+
const client = this.#client;
|
|
836
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
837
|
+
(0, _src_core.validateBrowserContextOptions)(options);
|
|
838
|
+
const params = { disposeOnDetach: false };
|
|
839
|
+
if (options?.proxy !== void 0) {
|
|
840
|
+
params["proxyServer"] = options.proxy.server;
|
|
841
|
+
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
842
|
+
}
|
|
843
|
+
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
844
|
+
const result = await client.send("Target.createBrowserContext", params);
|
|
845
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
846
|
+
const id = result["browserContextId"];
|
|
847
|
+
const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
|
|
848
|
+
try {
|
|
849
|
+
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
850
|
+
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
851
|
+
browserContextId: id,
|
|
852
|
+
downloadPath: options.downloads.path,
|
|
853
|
+
eventsEnabled: true
|
|
854
|
+
});
|
|
855
|
+
} catch (error) {
|
|
856
|
+
await context.close().catch(() => void 0);
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
this.#registerContext(context);
|
|
860
|
+
return context;
|
|
861
|
+
}
|
|
707
862
|
async create(options) {
|
|
708
863
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.#
|
|
864
|
+
const client = this.#client;
|
|
865
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
866
|
+
let context = this.#contexts[0];
|
|
867
|
+
if (context === void 0) {
|
|
868
|
+
context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
869
|
+
this.#registerContext(context);
|
|
714
870
|
}
|
|
715
|
-
const page = await
|
|
871
|
+
const page = await context.create(options);
|
|
716
872
|
this.#emitter.emit("page", page);
|
|
717
873
|
return page;
|
|
718
874
|
}
|
|
719
|
-
|
|
720
|
-
|
|
875
|
+
destroy() {
|
|
876
|
+
const active = this.#shutdown;
|
|
877
|
+
if (active !== void 0) return active;
|
|
878
|
+
if (this.#destroyed) return Promise.resolve();
|
|
721
879
|
this.#destroyed = true;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
await ctx.close();
|
|
727
|
-
} catch {}
|
|
728
|
-
this.#contexts = [];
|
|
729
|
-
await this.#terminate(this.#process);
|
|
730
|
-
this.#process = void 0;
|
|
731
|
-
if (this.#client !== void 0) {
|
|
732
|
-
try {
|
|
733
|
-
await this.#client.close();
|
|
734
|
-
} catch {}
|
|
735
|
-
this.#client = void 0;
|
|
736
|
-
}
|
|
737
|
-
} finally {
|
|
738
|
-
this.#status = "disconnected";
|
|
739
|
-
this.#connection = void 0;
|
|
740
|
-
this.#lastPid = void 0;
|
|
741
|
-
this.#emitter.emit("destroy");
|
|
742
|
-
this.#emitter.destroy();
|
|
743
|
-
}
|
|
880
|
+
this.#abort.abort();
|
|
881
|
+
const shutdown = this.#destroyResources();
|
|
882
|
+
this.#shutdown = shutdown;
|
|
883
|
+
return shutdown;
|
|
744
884
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
* @remarks
|
|
750
|
-
* Sends CDP `Browser.close` best-effort; on an owned browser also awaits
|
|
751
|
-
* the process's exit (bounded by the kill-escalation grace period,
|
|
752
|
-
* escalating to a kill only if it does not exit in time). Ends in the
|
|
753
|
-
* same local-cleanup state as `destroy()`.
|
|
754
|
-
*/
|
|
755
|
-
async close() {
|
|
756
|
-
if (this.#destroyed) return;
|
|
885
|
+
close() {
|
|
886
|
+
const active = this.#shutdown;
|
|
887
|
+
if (active !== void 0) return active;
|
|
888
|
+
if (this.#destroyed) return Promise.resolve();
|
|
757
889
|
this.#destroyed = true;
|
|
890
|
+
this.#abort.abort();
|
|
891
|
+
const shutdown = this.#closeResources();
|
|
892
|
+
this.#shutdown = shutdown;
|
|
893
|
+
return shutdown;
|
|
894
|
+
}
|
|
895
|
+
async #establish() {
|
|
896
|
+
this.#assertNotAborted();
|
|
897
|
+
this.#status = "connecting";
|
|
758
898
|
try {
|
|
759
|
-
this.#
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (client !== void 0) try {
|
|
763
|
-
await client.send("Browser.close");
|
|
764
|
-
} catch {}
|
|
765
|
-
const process = this.#process;
|
|
766
|
-
if (process !== void 0) {
|
|
767
|
-
const exited = new Promise((resolve) => {
|
|
768
|
-
process.once("exit", () => resolve());
|
|
769
|
-
});
|
|
770
|
-
let timer;
|
|
771
|
-
const timedOut = await Promise.race([exited.then(() => false), new Promise((resolve) => {
|
|
772
|
-
timer = setTimeout(() => resolve(true), BROWSER_KILL_GRACE_MS);
|
|
773
|
-
})]);
|
|
774
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
775
|
-
if (timedOut) await this.#terminate(process);
|
|
776
|
-
this.#process = void 0;
|
|
899
|
+
if (this.#owned === true && this.#endpoint !== void 0) {
|
|
900
|
+
await this.#connectCDP(this.#endpoint);
|
|
901
|
+
return;
|
|
777
902
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (client !== void 0) {
|
|
783
|
-
try {
|
|
784
|
-
await client.close();
|
|
785
|
-
} catch {}
|
|
786
|
-
this.#client = void 0;
|
|
903
|
+
const endpoint = this.#options.cdp?.endpoint;
|
|
904
|
+
if (endpoint !== void 0) {
|
|
905
|
+
await this.#connectCDP(endpoint);
|
|
906
|
+
return;
|
|
787
907
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
908
|
+
if (this.#options.cdp?.discover ?? true) {
|
|
909
|
+
const discovery = await this.#raceAbort(this.#discoverCDP(this.#signal()));
|
|
910
|
+
if (discovery.found && discovery.endpoint !== void 0) {
|
|
911
|
+
this.#engine = browserToEngine(discovery.browser);
|
|
912
|
+
await this.#connectCDP(discovery.endpoint);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
} else await this.#raceAbort(this.#assertPortFree());
|
|
916
|
+
this.#assertNotAborted();
|
|
917
|
+
await this.#launch();
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
920
|
+
this.#status = "error";
|
|
921
|
+
this.#emitter.emit("error", error);
|
|
922
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
923
|
+
throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), { executable: this.#options.executable });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async #detach() {
|
|
927
|
+
this.#status = "disconnected";
|
|
928
|
+
this.#connection = void 0;
|
|
929
|
+
this.#unbindTransport();
|
|
930
|
+
const client = this.#client;
|
|
931
|
+
this.#client = void 0;
|
|
932
|
+
await this.#destroyContexts();
|
|
933
|
+
this.#contexts = [];
|
|
934
|
+
if (this.#owned !== true) {
|
|
935
|
+
this.#owned = void 0;
|
|
936
|
+
this.#endpoint = void 0;
|
|
794
937
|
}
|
|
938
|
+
await this.#closeClient(client);
|
|
939
|
+
if (this.#destroyed) return;
|
|
940
|
+
this.#emitter.emit("disconnect");
|
|
941
|
+
this.#emitter.emit("idle");
|
|
795
942
|
}
|
|
796
943
|
#assertNotAborted() {
|
|
797
|
-
if (this.#
|
|
944
|
+
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted");
|
|
945
|
+
}
|
|
946
|
+
#signal() {
|
|
947
|
+
const external = this.#options.signal;
|
|
948
|
+
return external === void 0 ? this.#abort.signal : AbortSignal.any([this.#abort.signal, external]);
|
|
798
949
|
}
|
|
799
|
-
/**
|
|
800
|
-
* Race a promise against the connection's external AbortSignal (if any),
|
|
801
|
-
* rejecting promptly with a coded BrowserConnectionError when it fires.
|
|
802
|
-
*/
|
|
803
950
|
async #raceAbort(promise) {
|
|
804
|
-
const signal = this.#
|
|
805
|
-
if (signal === void 0) return promise;
|
|
951
|
+
const signal = this.#signal();
|
|
806
952
|
if (signal.aborted) throw new BrowserConnectionError("Connection aborted");
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
abortWon = true;
|
|
811
|
-
cleanup();
|
|
812
|
-
reject(new BrowserConnectionError("Connection aborted"));
|
|
813
|
-
};
|
|
814
|
-
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
815
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
816
|
-
promise.then((value) => {
|
|
817
|
-
cleanup();
|
|
818
|
-
resolve(value);
|
|
819
|
-
}, (error) => {
|
|
820
|
-
cleanup();
|
|
821
|
-
reject(error);
|
|
822
|
-
});
|
|
823
|
-
}).catch((error) => {
|
|
824
|
-
if (abortWon) promise.catch(() => void 0);
|
|
825
|
-
throw error;
|
|
953
|
+
const aborted = Promise.withResolvers();
|
|
954
|
+
const listener = (0, node_events.addAbortListener)(signal, () => {
|
|
955
|
+
aborted.reject(new BrowserConnectionError("Connection aborted"));
|
|
826
956
|
});
|
|
957
|
+
try {
|
|
958
|
+
return await Promise.race([promise, aborted.promise]);
|
|
959
|
+
} finally {
|
|
960
|
+
listener[Symbol.dispose]();
|
|
961
|
+
promise.catch(() => void 0);
|
|
962
|
+
}
|
|
827
963
|
}
|
|
828
|
-
#reset() {
|
|
829
|
-
this.#contexts = [];
|
|
830
|
-
this.#status = "disconnected";
|
|
831
|
-
this.#connection = void 0;
|
|
832
|
-
this.#client = void 0;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Handle the owned process exiting on its own (not via destroy()/close()).
|
|
836
|
-
* The process is already gone — no kill is attempted. Cleans up orphaned
|
|
837
|
-
* state so the next connect() call can start fresh, and emits a coded
|
|
838
|
-
* `error` (cause: process exit) before `disconnect`.
|
|
839
|
-
*/
|
|
840
964
|
#handleProcessExit() {
|
|
841
965
|
if (this.#destroyed) return;
|
|
842
|
-
|
|
966
|
+
const connected = this.#status === "connected";
|
|
967
|
+
const client = this.#client;
|
|
968
|
+
const contexts = this.#contexts;
|
|
843
969
|
this.#unbindTransport();
|
|
844
970
|
this.#unbindProcess();
|
|
845
|
-
const client = this.#client;
|
|
846
971
|
this.#process = void 0;
|
|
847
|
-
this.#
|
|
848
|
-
this.#
|
|
849
|
-
|
|
850
|
-
this.#
|
|
851
|
-
this.#
|
|
852
|
-
this.#
|
|
972
|
+
this.#releaseProfile().catch((error) => this.#emitter.emit("error", error));
|
|
973
|
+
this.#client = void 0;
|
|
974
|
+
this.#contexts = [];
|
|
975
|
+
this.#connection = void 0;
|
|
976
|
+
this.#owned = void 0;
|
|
977
|
+
this.#endpoint = void 0;
|
|
978
|
+
if (connected) this.#status = "disconnected";
|
|
979
|
+
this.#destroyContextList(contexts);
|
|
980
|
+
this.#closeClient(client);
|
|
981
|
+
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
982
|
+
if (connected) {
|
|
983
|
+
this.#emitter.emit("disconnect");
|
|
984
|
+
this.#emitter.emit("idle");
|
|
985
|
+
}
|
|
853
986
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
* still alive, or the connection is CDP-attached. Never kills the
|
|
857
|
-
* process — only the local client is reset, so the SAME instance can
|
|
858
|
-
* reconnect (e.g. rediscover on the port and reattach). Emits a coded
|
|
859
|
-
* `error` (cause: connection loss) before `disconnect`.
|
|
860
|
-
*/
|
|
861
|
-
#handleTransportLoss(deferred = false) {
|
|
862
|
-
if (this.#destroyed) return;
|
|
863
|
-
if (this.#status !== "connected") return;
|
|
987
|
+
#handleTransportLoss() {
|
|
988
|
+
if (this.#destroyed || this.#status !== "connected") return;
|
|
864
989
|
const process = this.#process;
|
|
865
990
|
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
866
991
|
this.#handleProcessExit();
|
|
867
992
|
return;
|
|
868
993
|
}
|
|
869
|
-
if (process !== void 0
|
|
870
|
-
setTimeout(() => this.#
|
|
994
|
+
if (process !== void 0) {
|
|
995
|
+
setTimeout(() => this.#confirmTransportLoss(), 50);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.#confirmTransportLoss();
|
|
999
|
+
}
|
|
1000
|
+
#confirmTransportLoss() {
|
|
1001
|
+
if (this.#destroyed || this.#status !== "connected") return;
|
|
1002
|
+
const process = this.#process;
|
|
1003
|
+
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
1004
|
+
this.#handleProcessExit();
|
|
871
1005
|
return;
|
|
872
1006
|
}
|
|
873
|
-
if (process
|
|
1007
|
+
if (process?.pid !== void 0) try {
|
|
874
1008
|
process.kill(0);
|
|
875
1009
|
} catch {
|
|
876
1010
|
this.#handleProcessExit();
|
|
877
1011
|
return;
|
|
878
1012
|
}
|
|
879
|
-
this.#unbindTransport();
|
|
880
1013
|
const client = this.#client;
|
|
881
|
-
this.#
|
|
882
|
-
|
|
883
|
-
this.#
|
|
1014
|
+
const owned = this.#owned === true;
|
|
1015
|
+
const contexts = this.#contexts;
|
|
1016
|
+
this.#unbindTransport();
|
|
1017
|
+
this.#client = void 0;
|
|
1018
|
+
this.#contexts = [];
|
|
1019
|
+
this.#connection = void 0;
|
|
1020
|
+
this.#status = "disconnected";
|
|
1021
|
+
if (!owned) {
|
|
1022
|
+
this.#owned = void 0;
|
|
1023
|
+
this.#endpoint = void 0;
|
|
1024
|
+
}
|
|
1025
|
+
this.#destroyContextList(contexts);
|
|
1026
|
+
this.#closeClient(client);
|
|
1027
|
+
this.#emitter.emit("error", new BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
|
|
884
1028
|
this.#emitter.emit("disconnect");
|
|
885
1029
|
this.#emitter.emit("idle");
|
|
886
1030
|
}
|
|
887
|
-
/** Subscribe to the CDP transport's close/error events for deterministic transport-loss detection. */
|
|
888
1031
|
#bindTransport(transport) {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
transport.emitter.on("close",
|
|
892
|
-
transport.emitter.on("error",
|
|
893
|
-
this.#transportUnbind = () => {
|
|
894
|
-
transport.emitter.off("close", onClose);
|
|
895
|
-
transport.emitter.off("error", onError);
|
|
896
|
-
};
|
|
1032
|
+
this.#unbindTransport();
|
|
1033
|
+
this.#transport = transport;
|
|
1034
|
+
transport.emitter.on("close", this.#onTransportClose);
|
|
1035
|
+
transport.emitter.on("error", this.#onTransportError);
|
|
897
1036
|
}
|
|
898
1037
|
#unbindTransport() {
|
|
899
|
-
this.#
|
|
900
|
-
|
|
1038
|
+
const transport = this.#transport;
|
|
1039
|
+
if (transport === void 0) return;
|
|
1040
|
+
transport.emitter.off("close", this.#onTransportClose);
|
|
1041
|
+
transport.emitter.off("error", this.#onTransportError);
|
|
1042
|
+
this.#transport = void 0;
|
|
901
1043
|
}
|
|
902
|
-
/** Subscribe to a launched process's exit event for deterministic process-exit detection. */
|
|
903
1044
|
#bindProcess(process) {
|
|
904
|
-
|
|
905
|
-
process.once("exit",
|
|
906
|
-
|
|
1045
|
+
this.#unbindProcess();
|
|
1046
|
+
process.once("exit", this.#onProcessExit);
|
|
1047
|
+
if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#onProcessExit);
|
|
907
1048
|
}
|
|
908
1049
|
#unbindProcess() {
|
|
909
|
-
this.#
|
|
910
|
-
this.#processUnbind = void 0;
|
|
1050
|
+
this.#process?.off("exit", this.#onProcessExit);
|
|
911
1051
|
}
|
|
912
1052
|
#timeout() {
|
|
913
1053
|
return this.#options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
|
|
914
1054
|
}
|
|
915
|
-
async #
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
const controller = new AbortController();
|
|
919
|
-
const timer = setTimeout(() => controller.abort(), this.#timeout());
|
|
1055
|
+
async #discoverCDP(signal) {
|
|
1056
|
+
const url = `${BROWSER_CDP_PROTOCOL}://${this.#cdpHost}:${this.#cdpPort}${BROWSER_CDP_VERSION_PATH}`;
|
|
1057
|
+
const requestSignal = signal === void 0 ? AbortSignal.timeout(this.#timeout()) : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout())]);
|
|
920
1058
|
try {
|
|
921
|
-
const response = await fetch(url, { signal:
|
|
922
|
-
if (!response.ok) return this.#
|
|
1059
|
+
const response = await fetch(url, { signal: requestSignal });
|
|
1060
|
+
if (!response.ok) return this.#notFound();
|
|
923
1061
|
const info = await response.json();
|
|
924
|
-
if (!(0, _orkestrel_contract.isRecord)(info)) return this.#
|
|
1062
|
+
if (!(0, _orkestrel_contract.isRecord)(info)) return this.#notFound();
|
|
925
1063
|
const endpoint = (0, _orkestrel_contract.isString)(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0;
|
|
926
|
-
const
|
|
1064
|
+
const browser = (0, _orkestrel_contract.isString)(info["Browser"]) ? info["Browser"] : void 0;
|
|
927
1065
|
return {
|
|
928
1066
|
found: endpoint !== void 0,
|
|
929
1067
|
endpoint,
|
|
930
|
-
browser
|
|
1068
|
+
browser,
|
|
931
1069
|
connection: endpoint !== void 0 ? "cdp" : void 0
|
|
932
1070
|
};
|
|
933
|
-
} catch {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
clearTimeout(timer);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
if (signal?.aborted === true) throw error;
|
|
1073
|
+
return this.#notFound();
|
|
937
1074
|
}
|
|
938
1075
|
}
|
|
939
|
-
#
|
|
1076
|
+
#notFound() {
|
|
940
1077
|
return {
|
|
941
1078
|
found: false,
|
|
942
1079
|
endpoint: void 0,
|
|
@@ -944,31 +1081,21 @@ var Browser = class {
|
|
|
944
1081
|
connection: void 0
|
|
945
1082
|
};
|
|
946
1083
|
}
|
|
947
|
-
/**
|
|
948
|
-
* Reject when something is already listening on the configured CDP port —
|
|
949
|
-
* used ahead of a `discover: false` launch, so a caller demanding a fresh
|
|
950
|
-
* browser never silently attaches to a stranger already on that port.
|
|
951
|
-
*/
|
|
952
1084
|
async #assertPortFree() {
|
|
953
1085
|
if (await this.#probePort()) throw new BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
|
|
954
1086
|
port: this.#cdpPort,
|
|
955
1087
|
host: this.#cdpHost
|
|
956
1088
|
});
|
|
957
1089
|
}
|
|
958
|
-
/** Very short probe of the CDP version endpoint — just enough to detect an occupied port, not full discovery. */
|
|
959
1090
|
async #probePort() {
|
|
960
1091
|
const url = `${BROWSER_CDP_PROTOCOL}://${this.#cdpHost}:${this.#cdpPort}${BROWSER_CDP_VERSION_PATH}`;
|
|
961
|
-
const controller = new AbortController();
|
|
962
|
-
const timer = setTimeout(() => controller.abort(), 200);
|
|
963
1092
|
try {
|
|
964
|
-
return (await fetch(url, { signal:
|
|
1093
|
+
return (await fetch(url, { signal: AbortSignal.timeout(200) })).ok;
|
|
965
1094
|
} catch {
|
|
966
1095
|
return false;
|
|
967
|
-
} finally {
|
|
968
|
-
clearTimeout(timer);
|
|
969
1096
|
}
|
|
970
1097
|
}
|
|
971
|
-
async #
|
|
1098
|
+
async #connectCDP(endpoint) {
|
|
972
1099
|
const transport = createCDPTransport({
|
|
973
1100
|
url: endpoint,
|
|
974
1101
|
timeout: this.#timeout()
|
|
@@ -977,18 +1104,27 @@ var Browser = class {
|
|
|
977
1104
|
transport,
|
|
978
1105
|
timeout: this.#timeout()
|
|
979
1106
|
});
|
|
1107
|
+
const retained = this.#owned === true && this.#endpoint === endpoint;
|
|
980
1108
|
try {
|
|
981
1109
|
await this.#raceAbort(client.connect());
|
|
982
1110
|
this.#client = client;
|
|
983
|
-
this.#
|
|
1111
|
+
this.#endpoint = endpoint;
|
|
1112
|
+
this.#connection = retained ? this.#retainedConnection() : "cdp";
|
|
1113
|
+
this.#owned = retained;
|
|
984
1114
|
this.#status = "connected";
|
|
985
1115
|
this.#bindTransport(transport);
|
|
986
1116
|
await this.#syncContexts();
|
|
987
|
-
this.#emitter.emit("connect",
|
|
1117
|
+
this.#emitter.emit("connect", this.#connection);
|
|
988
1118
|
} catch (error) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1119
|
+
this.#unbindTransport();
|
|
1120
|
+
if (this.#client === client) this.#client = void 0;
|
|
1121
|
+
this.#contexts = [];
|
|
1122
|
+
this.#connection = void 0;
|
|
1123
|
+
if (!retained) {
|
|
1124
|
+
this.#owned = void 0;
|
|
1125
|
+
this.#endpoint = void 0;
|
|
1126
|
+
}
|
|
1127
|
+
await this.#closeClient(client);
|
|
992
1128
|
throw error;
|
|
993
1129
|
}
|
|
994
1130
|
}
|
|
@@ -1006,18 +1142,24 @@ var Browser = class {
|
|
|
1006
1142
|
executable = found?.executable;
|
|
1007
1143
|
resolvedEngine = found?.engine;
|
|
1008
1144
|
}
|
|
1009
|
-
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine
|
|
1145
|
+
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
1010
1146
|
this.#engine = resolvedEngine ?? "chromium";
|
|
1011
|
-
|
|
1012
|
-
const profile = this.#options.profile;
|
|
1013
|
-
|
|
1014
|
-
|
|
1147
|
+
await this.#releaseProfile();
|
|
1148
|
+
const profile = await createBrowserProfile(this.#options.profile);
|
|
1149
|
+
this.#profile = profile;
|
|
1150
|
+
let process;
|
|
1151
|
+
try {
|
|
1152
|
+
process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
await this.#releaseProfile();
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1015
1157
|
this.#process = process;
|
|
1016
|
-
let transport;
|
|
1017
1158
|
let client;
|
|
1018
1159
|
try {
|
|
1019
|
-
|
|
1020
|
-
|
|
1160
|
+
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
1161
|
+
const transport = createCDPTransport({
|
|
1162
|
+
url: endpoint,
|
|
1021
1163
|
timeout: this.#timeout()
|
|
1022
1164
|
});
|
|
1023
1165
|
client = new _src_core.CDPClient({
|
|
@@ -1025,20 +1167,27 @@ var Browser = class {
|
|
|
1025
1167
|
timeout: this.#timeout()
|
|
1026
1168
|
});
|
|
1027
1169
|
await this.#raceAbort(client.connect());
|
|
1170
|
+
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1028
1171
|
this.#client = client;
|
|
1029
|
-
this.#
|
|
1172
|
+
this.#endpoint = endpoint;
|
|
1173
|
+
this.#connection = connection;
|
|
1174
|
+
this.#owned = true;
|
|
1030
1175
|
this.#status = "connected";
|
|
1031
1176
|
this.#bindTransport(transport);
|
|
1032
1177
|
this.#bindProcess(process);
|
|
1033
1178
|
await this.#syncContexts();
|
|
1034
1179
|
this.#emitter.emit("launch", this.#engine);
|
|
1035
|
-
this.#emitter.emit("connect",
|
|
1180
|
+
this.#emitter.emit("connect", connection);
|
|
1036
1181
|
} catch (error) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1182
|
+
this.#unbindTransport();
|
|
1183
|
+
this.#unbindProcess();
|
|
1184
|
+
await this.#closeClient(client);
|
|
1040
1185
|
await this.#terminate(process);
|
|
1186
|
+
await this.#releaseProfile();
|
|
1041
1187
|
this.#process = void 0;
|
|
1188
|
+
this.#client = void 0;
|
|
1189
|
+
this.#endpoint = void 0;
|
|
1190
|
+
this.#owned = void 0;
|
|
1042
1191
|
throw error;
|
|
1043
1192
|
}
|
|
1044
1193
|
}
|
|
@@ -1047,72 +1196,209 @@ var Browser = class {
|
|
|
1047
1196
|
executable,
|
|
1048
1197
|
args
|
|
1049
1198
|
};
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
callback();
|
|
1058
|
-
};
|
|
1059
|
-
const onError = (error) => {
|
|
1060
|
-
finish(() => reject(new BrowserConnectionError(error.message, context)));
|
|
1061
|
-
};
|
|
1062
|
-
const onExit = (code, signal) => {
|
|
1063
|
-
finish(() => reject(new BrowserConnectionError(this.#formatLaunchExit(code, signal), context)));
|
|
1064
|
-
};
|
|
1065
|
-
process.once("error", onError);
|
|
1066
|
-
process.once("exit", onExit);
|
|
1067
|
-
waitForCdpReady(this.#cdpPort, this.#timeout(), this.#cdpHost).then((wsUrl) => {
|
|
1068
|
-
finish(() => resolve(wsUrl));
|
|
1069
|
-
}, (error) => {
|
|
1070
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1071
|
-
finish(() => reject(new BrowserConnectionError(message, context)));
|
|
1072
|
-
});
|
|
1199
|
+
const controller = new AbortController();
|
|
1200
|
+
const signal = AbortSignal.any([controller.signal, this.#signal()]);
|
|
1201
|
+
const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
|
|
1202
|
+
const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
|
|
1203
|
+
const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
|
|
1204
|
+
const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
|
|
1205
|
+
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1073
1206
|
});
|
|
1207
|
+
try {
|
|
1208
|
+
return await Promise.race([ready, exited]);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
1211
|
+
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
|
|
1212
|
+
throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), context);
|
|
1213
|
+
} finally {
|
|
1214
|
+
controller.abort();
|
|
1215
|
+
ready.catch(() => void 0);
|
|
1216
|
+
exited.catch(() => void 0);
|
|
1217
|
+
}
|
|
1074
1218
|
}
|
|
1075
1219
|
#formatLaunchExit(code, signal) {
|
|
1076
1220
|
if (signal !== null) return `Browser process exited before CDP became ready (signal: ${signal})`;
|
|
1077
1221
|
if (code !== null) return `Browser process exited before CDP became ready (code: ${code})`;
|
|
1078
1222
|
return "Browser process exited before CDP became ready";
|
|
1079
1223
|
}
|
|
1224
|
+
#retainedConnection() {
|
|
1225
|
+
if (this.#process === void 0) return "cdp";
|
|
1226
|
+
return this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1227
|
+
}
|
|
1080
1228
|
async #syncContexts() {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
await
|
|
1086
|
-
|
|
1229
|
+
const client = this.#client;
|
|
1230
|
+
if (client === void 0) return;
|
|
1231
|
+
let result;
|
|
1232
|
+
try {
|
|
1233
|
+
result = await client.send("Target.getTargets");
|
|
1234
|
+
} catch {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
|
|
1238
|
+
const pages = [];
|
|
1239
|
+
for (const target of result["targetInfos"]) {
|
|
1240
|
+
if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || target["type"] !== "page" || !(0, _orkestrel_contract.isString)(target["title"]) || !(0, _orkestrel_contract.isString)(target["url"])) continue;
|
|
1241
|
+
pages.push({
|
|
1242
|
+
id: target["targetId"],
|
|
1243
|
+
type: target["type"],
|
|
1244
|
+
title: target["title"],
|
|
1245
|
+
url: target["url"]
|
|
1246
|
+
});
|
|
1087
1247
|
}
|
|
1248
|
+
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
1249
|
+
const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
1250
|
+
await context.sync(pages);
|
|
1251
|
+
this.#registerContext(context);
|
|
1088
1252
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1253
|
+
async #destroyResources() {
|
|
1254
|
+
try {
|
|
1255
|
+
await this.#settle();
|
|
1256
|
+
this.#unbindTransport();
|
|
1257
|
+
this.#unbindProcess();
|
|
1258
|
+
const client = this.#client;
|
|
1259
|
+
if (this.#owned === true) {
|
|
1260
|
+
await this.#closeContexts();
|
|
1261
|
+
if (this.#process === void 0) await this.#closeRemote(client);
|
|
1262
|
+
} else await this.#destroyContexts();
|
|
1263
|
+
this.#contexts = [];
|
|
1264
|
+
await this.#terminate(this.#process);
|
|
1265
|
+
this.#process = void 0;
|
|
1266
|
+
await this.#closeClient(client);
|
|
1267
|
+
this.#client = void 0;
|
|
1268
|
+
} finally {
|
|
1269
|
+
try {
|
|
1270
|
+
await this.#releaseProfile();
|
|
1271
|
+
} finally {
|
|
1272
|
+
this.#finish();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async #closeResources() {
|
|
1277
|
+
try {
|
|
1278
|
+
await this.#settle();
|
|
1279
|
+
this.#unbindTransport();
|
|
1280
|
+
this.#unbindProcess();
|
|
1281
|
+
const client = this.#client;
|
|
1282
|
+
await this.#closeContexts();
|
|
1283
|
+
this.#contexts = [];
|
|
1284
|
+
await this.#closeRemote(client);
|
|
1285
|
+
const process = this.#process;
|
|
1286
|
+
if (process !== void 0) {
|
|
1287
|
+
if (!await this.#waitForProcessWithin(process, 3e3)) await this.#terminate(process);
|
|
1288
|
+
}
|
|
1289
|
+
this.#process = void 0;
|
|
1290
|
+
await this.#closeClient(client);
|
|
1291
|
+
this.#client = void 0;
|
|
1292
|
+
} finally {
|
|
1293
|
+
try {
|
|
1294
|
+
await this.#releaseProfile();
|
|
1295
|
+
} finally {
|
|
1296
|
+
this.#finish();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async #settle() {
|
|
1301
|
+
await this.#connecting?.catch(() => void 0);
|
|
1302
|
+
await this.#disconnecting?.catch(() => void 0);
|
|
1303
|
+
}
|
|
1304
|
+
async #closeRemote(client) {
|
|
1305
|
+
let remote = client;
|
|
1306
|
+
let temporary = false;
|
|
1307
|
+
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
1308
|
+
remote = new _src_core.CDPClient({
|
|
1309
|
+
transport: createCDPTransport({
|
|
1310
|
+
url: this.#endpoint,
|
|
1311
|
+
timeout: this.#timeout()
|
|
1312
|
+
}),
|
|
1313
|
+
timeout: this.#timeout()
|
|
1314
|
+
});
|
|
1315
|
+
try {
|
|
1316
|
+
await remote.connect();
|
|
1317
|
+
temporary = true;
|
|
1318
|
+
} catch {
|
|
1319
|
+
await this.#closeClient(remote);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (remote !== void 0) try {
|
|
1324
|
+
await remote.send("Browser.close");
|
|
1325
|
+
} catch {}
|
|
1326
|
+
if (temporary) await this.#closeClient(remote);
|
|
1327
|
+
}
|
|
1328
|
+
async #closeContexts() {
|
|
1329
|
+
for (const context of [...this.#contexts]) try {
|
|
1330
|
+
await context.close();
|
|
1331
|
+
} catch {}
|
|
1332
|
+
}
|
|
1333
|
+
async #destroyContexts() {
|
|
1334
|
+
await this.#destroyContextList([...this.#contexts]);
|
|
1335
|
+
}
|
|
1336
|
+
#registerContext(context) {
|
|
1337
|
+
this.#contexts.push(context);
|
|
1338
|
+
context.emitter.on("close", () => {
|
|
1339
|
+
const index = this.#contexts.indexOf(context);
|
|
1340
|
+
if (index >= 0) this.#contexts.splice(index, 1);
|
|
1099
1341
|
});
|
|
1342
|
+
this.#emitter.emit("context", context);
|
|
1343
|
+
}
|
|
1344
|
+
async #destroyContextList(contexts) {
|
|
1345
|
+
for (const context of contexts) try {
|
|
1346
|
+
await context.destroy();
|
|
1347
|
+
} catch {}
|
|
1348
|
+
}
|
|
1349
|
+
async #closeClient(client) {
|
|
1350
|
+
if (client === void 0) return;
|
|
1351
|
+
try {
|
|
1352
|
+
await client.close();
|
|
1353
|
+
} catch {}
|
|
1354
|
+
}
|
|
1355
|
+
async #releaseProfile() {
|
|
1356
|
+
const profile = this.#profile;
|
|
1357
|
+
this.#profile = void 0;
|
|
1358
|
+
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1359
|
+
}
|
|
1360
|
+
async #waitForProcess(process) {
|
|
1361
|
+
if (process.exitCode !== null || process.signalCode !== null) return;
|
|
1362
|
+
await (0, node_events.once)(process, "exit");
|
|
1363
|
+
}
|
|
1364
|
+
async #waitForProcessWithin(process, timeout) {
|
|
1365
|
+
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
1366
|
+
const signal = AbortSignal.timeout(timeout);
|
|
1367
|
+
try {
|
|
1368
|
+
await (0, node_events.once)(process, "exit", { signal });
|
|
1369
|
+
return true;
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
if (signal.aborted) return process.exitCode !== null || process.signalCode !== null;
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async #terminate(process) {
|
|
1376
|
+
if (process === void 0 || process.exitCode !== null || process.signalCode !== null) return;
|
|
1100
1377
|
try {
|
|
1101
1378
|
process.kill("SIGTERM");
|
|
1102
1379
|
} catch {
|
|
1103
1380
|
return;
|
|
1104
1381
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
if (timedOut) {
|
|
1111
|
-
try {
|
|
1112
|
-
process.kill("SIGKILL");
|
|
1113
|
-
} catch {}
|
|
1114
|
-
await exited;
|
|
1382
|
+
if (await this.#waitForProcessWithin(process, 3e3)) return;
|
|
1383
|
+
try {
|
|
1384
|
+
process.kill("SIGKILL");
|
|
1385
|
+
} catch {
|
|
1386
|
+
return;
|
|
1115
1387
|
}
|
|
1388
|
+
await this.#waitForProcess(process);
|
|
1389
|
+
}
|
|
1390
|
+
#finish() {
|
|
1391
|
+
this.#unbindTransport();
|
|
1392
|
+
this.#unbindProcess();
|
|
1393
|
+
this.#status = "disconnected";
|
|
1394
|
+
this.#connection = void 0;
|
|
1395
|
+
this.#owned = void 0;
|
|
1396
|
+
this.#endpoint = void 0;
|
|
1397
|
+
this.#client = void 0;
|
|
1398
|
+
this.#process = void 0;
|
|
1399
|
+
this.#contexts = [];
|
|
1400
|
+
this.#emitter.emit("destroy");
|
|
1401
|
+
this.#emitter.destroy();
|
|
1116
1402
|
}
|
|
1117
1403
|
};
|
|
1118
1404
|
//#endregion
|
|
@@ -1128,13 +1414,15 @@ exports.BROWSER_EXECUTABLE_PATHS = BROWSER_EXECUTABLE_PATHS;
|
|
|
1128
1414
|
exports.BROWSER_HEADLESS_ARG = BROWSER_HEADLESS_ARG;
|
|
1129
1415
|
exports.BROWSER_KILL_GRACE_MS = BROWSER_KILL_GRACE_MS;
|
|
1130
1416
|
exports.BROWSER_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS;
|
|
1131
|
-
exports.BROWSER_NOT_FOUND_RESULT = BROWSER_NOT_FOUND_RESULT;
|
|
1132
1417
|
exports.BROWSER_PORT_PROBE_TIMEOUT_MS = BROWSER_PORT_PROBE_TIMEOUT_MS;
|
|
1418
|
+
exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
|
|
1419
|
+
exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
|
|
1133
1420
|
exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
|
|
1134
1421
|
exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
|
|
1135
1422
|
exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
|
|
1136
1423
|
exports.BROWSER_STORE_GLOBS = BROWSER_STORE_GLOBS;
|
|
1137
1424
|
exports.BROWSER_STORE_LINK_NAME = BROWSER_STORE_LINK_NAME;
|
|
1425
|
+
exports.BROWSER_TRANSPORT_LOSS_CAUSE = BROWSER_TRANSPORT_LOSS_CAUSE;
|
|
1138
1426
|
exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
|
|
1139
1427
|
exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
|
|
1140
1428
|
exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
|
|
@@ -1145,11 +1433,12 @@ exports.BrowserNotConnectedError = BrowserNotConnectedError;
|
|
|
1145
1433
|
exports.WebSocketCDPTransport = WebSocketCDPTransport;
|
|
1146
1434
|
exports.browserToEngine = browserToEngine;
|
|
1147
1435
|
exports.createBrowser = createBrowser;
|
|
1436
|
+
exports.createBrowserProfile = createBrowserProfile;
|
|
1148
1437
|
exports.createCDPTransport = createCDPTransport;
|
|
1149
1438
|
exports.createScreenshotWriter = createScreenshotWriter;
|
|
1150
1439
|
exports.defaultInstallPaths = defaultInstallPaths;
|
|
1151
1440
|
exports.defaultStoreBases = defaultStoreBases;
|
|
1152
|
-
exports.
|
|
1441
|
+
exports.fetchCDPTargets = fetchCDPTargets;
|
|
1153
1442
|
exports.findAllEnvOverrides = findAllEnvOverrides;
|
|
1154
1443
|
exports.findAllInStore = findAllInStore;
|
|
1155
1444
|
exports.findAllInstallPaths = findAllInstallPaths;
|
|
@@ -1166,7 +1455,8 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
|
1166
1455
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
1167
1456
|
exports.probeAllPathNames = probeAllPathNames;
|
|
1168
1457
|
exports.probePathNames = probePathNames;
|
|
1169
|
-
exports.
|
|
1458
|
+
exports.removeBrowserProfile = removeBrowserProfile;
|
|
1459
|
+
exports.waitForCDPReady = waitForCDPReady;
|
|
1170
1460
|
exports.windowsRoots = windowsRoots;
|
|
1171
1461
|
|
|
1172
1462
|
//# sourceMappingURL=index.cjs.map
|