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