@orkestrel/browser 0.0.5 → 0.0.7
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 +6602 -629
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2906 -309
- package/dist/src/core/index.d.ts +2906 -309
- package/dist/src/core/index.js +6452 -631
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +261 -75
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +53 -5
- package/dist/src/server/index.d.ts +53 -5
- package/dist/src/server/index.js +261 -78
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -16
package/dist/src/server/index.js
CHANGED
|
@@ -1,16 +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
8
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
-
import { isRecord, isString } from "@orkestrel/contract";
|
|
7
9
|
import { randomBytes } from "node:crypto";
|
|
8
10
|
import { request } from "node:http";
|
|
9
11
|
import { request as request$1 } from "node:https";
|
|
10
12
|
import { Emitter } from "@orkestrel/emitter";
|
|
11
13
|
import { WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
|
|
12
14
|
import { addAbortListener, once } from "node:events";
|
|
13
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
15
|
//#region src/server/constants.ts
|
|
15
16
|
/** Default CDP port probed for an existing browser and used for launches. */
|
|
16
17
|
var BROWSER_DEFAULT_CDP_PORT = 9222;
|
|
@@ -26,7 +27,9 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
|
26
27
|
var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
|
|
27
28
|
/** Flag enabling headless mode on a launched browser process. */
|
|
28
29
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
29
|
-
/**
|
|
30
|
+
/** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
31
|
+
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
32
|
+
/** Bound for each launched-process exit window during TERM-to-KILL teardown. */
|
|
30
33
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
31
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. */
|
|
32
35
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
@@ -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
|
|
@@ -263,6 +266,44 @@ function browserToEngine(browser) {
|
|
|
263
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];
|
|
@@ -353,6 +394,10 @@ function findAllInStore(base, platform) {
|
|
|
353
394
|
/**
|
|
354
395
|
* Launch a browser process with raw-CDP debugging flags.
|
|
355
396
|
*
|
|
397
|
+
* @remarks
|
|
398
|
+
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
399
|
+
* signal and await every Chromium subprocess without affecting the caller.
|
|
400
|
+
*
|
|
356
401
|
* @param executable - Absolute path to the browser executable
|
|
357
402
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
358
403
|
* @param headless - Whether to launch in headless mode
|
|
@@ -366,7 +411,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
366
411
|
args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
|
|
367
412
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
368
413
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
369
|
-
return spawn(executable, args, {
|
|
414
|
+
return spawn(executable, args, {
|
|
415
|
+
stdio: "ignore",
|
|
416
|
+
detached: process.platform !== "win32"
|
|
417
|
+
});
|
|
370
418
|
}
|
|
371
419
|
/**
|
|
372
420
|
* Poll a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
@@ -415,7 +463,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
415
463
|
const response = await fetch(url, { signal: controller.signal });
|
|
416
464
|
if (!response.ok) return [];
|
|
417
465
|
const list = await response.json();
|
|
418
|
-
if (!
|
|
466
|
+
if (!isArray(list)) return [];
|
|
419
467
|
const targets = [];
|
|
420
468
|
for (const entry of list) {
|
|
421
469
|
if (!isRecord(entry)) continue;
|
|
@@ -457,11 +505,11 @@ var WebSocketCDPTransport = class {
|
|
|
457
505
|
#reject;
|
|
458
506
|
#timer;
|
|
459
507
|
#closeResolve;
|
|
460
|
-
#onSocketClose =
|
|
508
|
+
#onSocketClose = this.#handleSocketClose.bind(this);
|
|
461
509
|
constructor(options) {
|
|
462
510
|
this.#emitter = new Emitter({
|
|
463
|
-
on: options.on,
|
|
464
|
-
error: options.error
|
|
511
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
512
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
465
513
|
});
|
|
466
514
|
this.#url = options.url;
|
|
467
515
|
this.#timeout = options.timeout ?? BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -479,12 +527,12 @@ var WebSocketCDPTransport = class {
|
|
|
479
527
|
}
|
|
480
528
|
this.#socket?.destroy();
|
|
481
529
|
this.#socket = void 0;
|
|
482
|
-
const
|
|
483
|
-
this.#starting =
|
|
530
|
+
const transition = this.#open();
|
|
531
|
+
this.#starting = transition;
|
|
484
532
|
try {
|
|
485
|
-
await
|
|
533
|
+
await transition;
|
|
486
534
|
} finally {
|
|
487
|
-
if (this.#starting ===
|
|
535
|
+
if (this.#starting === transition) this.#starting = void 0;
|
|
488
536
|
}
|
|
489
537
|
}
|
|
490
538
|
async send(data) {
|
|
@@ -498,16 +546,21 @@ var WebSocketCDPTransport = class {
|
|
|
498
546
|
await active;
|
|
499
547
|
return;
|
|
500
548
|
}
|
|
501
|
-
const
|
|
502
|
-
this.#closing =
|
|
549
|
+
const transition = this.#stop();
|
|
550
|
+
this.#closing = transition;
|
|
503
551
|
try {
|
|
504
|
-
await
|
|
552
|
+
await transition;
|
|
505
553
|
} finally {
|
|
506
|
-
if (this.#closing ===
|
|
554
|
+
if (this.#closing === transition) this.#closing = void 0;
|
|
507
555
|
}
|
|
508
556
|
}
|
|
509
557
|
async #open() {
|
|
510
|
-
const
|
|
558
|
+
const parsed = attempt(() => new URL(this.#url));
|
|
559
|
+
if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
560
|
+
url: this.#url,
|
|
561
|
+
error: parsed.error
|
|
562
|
+
});
|
|
563
|
+
const url = parsed.value;
|
|
511
564
|
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
512
565
|
const key = randomBytes(16).toString("base64");
|
|
513
566
|
const deferred = Promise.withResolvers();
|
|
@@ -627,6 +680,9 @@ var WebSocketCDPTransport = class {
|
|
|
627
680
|
});
|
|
628
681
|
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
629
682
|
}
|
|
683
|
+
#handleSocketClose() {
|
|
684
|
+
this.#closeResolve?.();
|
|
685
|
+
}
|
|
630
686
|
};
|
|
631
687
|
//#endregion
|
|
632
688
|
//#region src/server/factories.ts
|
|
@@ -669,6 +725,9 @@ function createScreenshotWriter() {
|
|
|
669
725
|
}
|
|
670
726
|
//#endregion
|
|
671
727
|
//#region src/server/Browser.ts
|
|
728
|
+
/**
|
|
729
|
+
* Discovers, launches, connects to, and owns Chromium-family browser sessions.
|
|
730
|
+
*/
|
|
672
731
|
var Browser = class {
|
|
673
732
|
#emitter;
|
|
674
733
|
#options;
|
|
@@ -682,19 +741,21 @@ var Browser = class {
|
|
|
682
741
|
#endpoint;
|
|
683
742
|
#client;
|
|
684
743
|
#process;
|
|
744
|
+
#profile;
|
|
685
745
|
#contexts = [];
|
|
686
746
|
#destroyed = false;
|
|
687
747
|
#connecting;
|
|
688
748
|
#disconnecting;
|
|
749
|
+
#exitCleanup;
|
|
689
750
|
#shutdown;
|
|
690
751
|
#transport;
|
|
691
|
-
#onTransportClose =
|
|
692
|
-
#onTransportError =
|
|
693
|
-
#onProcessExit =
|
|
752
|
+
#onTransportClose = this.#handleTransportLoss.bind(this);
|
|
753
|
+
#onTransportError = this.#handleTransportLoss.bind(this);
|
|
754
|
+
#onProcessExit = this.#handleProcessExit.bind(this);
|
|
694
755
|
constructor(options) {
|
|
695
756
|
this.#emitter = new Emitter({
|
|
696
|
-
on: options
|
|
697
|
-
error: options
|
|
757
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
758
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
698
759
|
});
|
|
699
760
|
this.#options = options ?? {};
|
|
700
761
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
@@ -730,14 +791,12 @@ var Browser = class {
|
|
|
730
791
|
}
|
|
731
792
|
async connect() {
|
|
732
793
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
733
|
-
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
734
|
-
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
735
|
-
if (this.#status === "connected") return;
|
|
736
794
|
const active = this.#connecting;
|
|
737
795
|
if (active !== void 0) {
|
|
738
796
|
await active;
|
|
739
797
|
return;
|
|
740
798
|
}
|
|
799
|
+
if (this.#status === "connected") return;
|
|
741
800
|
const attempt = this.#establish();
|
|
742
801
|
this.#connecting = attempt;
|
|
743
802
|
try {
|
|
@@ -753,15 +812,11 @@ var Browser = class {
|
|
|
753
812
|
}
|
|
754
813
|
async disconnect() {
|
|
755
814
|
if (this.#destroyed) return;
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (this.#connecting !== void 0) {
|
|
762
|
-
await this.#connecting.catch(() => void 0);
|
|
763
|
-
if (this.#destroyed) return;
|
|
764
|
-
}
|
|
815
|
+
const disconnecting = this.#disconnecting;
|
|
816
|
+
const connecting = this.#connecting;
|
|
817
|
+
if (disconnecting !== void 0) await disconnecting;
|
|
818
|
+
if (connecting !== void 0) await connecting.catch(() => void 0);
|
|
819
|
+
if (this.#destroyed) return;
|
|
765
820
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
766
821
|
const attempt = this.#detach();
|
|
767
822
|
this.#disconnecting = attempt;
|
|
@@ -779,6 +834,35 @@ var Browser = class {
|
|
|
779
834
|
contexts() {
|
|
780
835
|
return [...this.#contexts];
|
|
781
836
|
}
|
|
837
|
+
async isolate(options) {
|
|
838
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
839
|
+
const client = this.#client;
|
|
840
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
841
|
+
validateBrowserContextOptions(options);
|
|
842
|
+
const params = { disposeOnDetach: false };
|
|
843
|
+
if (options?.proxy !== void 0) {
|
|
844
|
+
params["proxyServer"] = options.proxy.server;
|
|
845
|
+
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
846
|
+
}
|
|
847
|
+
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
848
|
+
const result = await client.send("Target.createBrowserContext", params);
|
|
849
|
+
if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
850
|
+
const id = result["browserContextId"];
|
|
851
|
+
const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
|
|
852
|
+
try {
|
|
853
|
+
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
854
|
+
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
855
|
+
browserContextId: id,
|
|
856
|
+
downloadPath: options.downloads.path,
|
|
857
|
+
eventsEnabled: true
|
|
858
|
+
});
|
|
859
|
+
} catch (error) {
|
|
860
|
+
await context.close().catch(() => void 0);
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
this.#registerContext(context);
|
|
864
|
+
return context;
|
|
865
|
+
}
|
|
782
866
|
async create(options) {
|
|
783
867
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
784
868
|
const client = this.#client;
|
|
@@ -786,7 +870,7 @@ var Browser = class {
|
|
|
786
870
|
let context = this.#contexts[0];
|
|
787
871
|
if (context === void 0) {
|
|
788
872
|
context = new BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
789
|
-
this.#
|
|
873
|
+
this.#registerContext(context);
|
|
790
874
|
}
|
|
791
875
|
const page = await context.create(options);
|
|
792
876
|
this.#emitter.emit("page", page);
|
|
@@ -813,6 +897,10 @@ var Browser = class {
|
|
|
813
897
|
return shutdown;
|
|
814
898
|
}
|
|
815
899
|
async #establish() {
|
|
900
|
+
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
901
|
+
await this.#settleExit();
|
|
902
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
903
|
+
if (this.#status === "connected") return;
|
|
816
904
|
this.#assertNotAborted();
|
|
817
905
|
this.#status = "connecting";
|
|
818
906
|
try {
|
|
@@ -839,8 +927,8 @@ var Browser = class {
|
|
|
839
927
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
840
928
|
this.#status = "error";
|
|
841
929
|
this.#emitter.emit("error", error);
|
|
842
|
-
if (error
|
|
843
|
-
throw new BrowserConnectionError(error
|
|
930
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
931
|
+
throw new BrowserConnectionError(isError(error) ? error.message : String(error), { executable: this.#options.executable });
|
|
844
932
|
}
|
|
845
933
|
}
|
|
846
934
|
async #detach() {
|
|
@@ -886,17 +974,19 @@ var Browser = class {
|
|
|
886
974
|
const connected = this.#status === "connected";
|
|
887
975
|
const client = this.#client;
|
|
888
976
|
const contexts = this.#contexts;
|
|
977
|
+
const process = this.#process;
|
|
889
978
|
this.#unbindTransport();
|
|
890
979
|
this.#unbindProcess();
|
|
891
980
|
this.#process = void 0;
|
|
981
|
+
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
982
|
+
this.#exitCleanup = cleanup;
|
|
983
|
+
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
892
984
|
this.#client = void 0;
|
|
893
985
|
this.#contexts = [];
|
|
894
986
|
this.#connection = void 0;
|
|
895
987
|
this.#owned = void 0;
|
|
896
988
|
this.#endpoint = void 0;
|
|
897
989
|
if (connected) this.#status = "disconnected";
|
|
898
|
-
this.#destroyContextList(contexts);
|
|
899
|
-
this.#closeClient(client);
|
|
900
990
|
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
901
991
|
if (connected) {
|
|
902
992
|
this.#emitter.emit("disconnect");
|
|
@@ -1056,15 +1146,23 @@ var Browser = class {
|
|
|
1056
1146
|
else {
|
|
1057
1147
|
const found = findSystemBrowser({
|
|
1058
1148
|
...this.#options.browsers,
|
|
1059
|
-
engine: requestedEngine
|
|
1149
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
1060
1150
|
});
|
|
1061
1151
|
executable = found?.executable;
|
|
1062
1152
|
resolvedEngine = found?.engine;
|
|
1063
1153
|
}
|
|
1064
1154
|
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
1065
1155
|
this.#engine = resolvedEngine ?? "chromium";
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1156
|
+
await this.#releaseProfile();
|
|
1157
|
+
const profile = await createBrowserProfile(this.#options.profile);
|
|
1158
|
+
this.#profile = profile;
|
|
1159
|
+
let process;
|
|
1160
|
+
try {
|
|
1161
|
+
process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
await this.#releaseProfile();
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1068
1166
|
this.#process = process;
|
|
1069
1167
|
let client;
|
|
1070
1168
|
try {
|
|
@@ -1078,7 +1176,7 @@ var Browser = class {
|
|
|
1078
1176
|
timeout: this.#timeout()
|
|
1079
1177
|
});
|
|
1080
1178
|
await this.#raceAbort(client.connect());
|
|
1081
|
-
const connection = profile === void 0 ? "launch" : "persistent";
|
|
1179
|
+
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1082
1180
|
this.#client = client;
|
|
1083
1181
|
this.#endpoint = endpoint;
|
|
1084
1182
|
this.#connection = connection;
|
|
@@ -1094,6 +1192,7 @@ var Browser = class {
|
|
|
1094
1192
|
this.#unbindProcess();
|
|
1095
1193
|
await this.#closeClient(client);
|
|
1096
1194
|
await this.#terminate(process);
|
|
1195
|
+
await this.#releaseProfile();
|
|
1097
1196
|
this.#process = void 0;
|
|
1098
1197
|
this.#client = void 0;
|
|
1099
1198
|
this.#endpoint = void 0;
|
|
@@ -1110,16 +1209,16 @@ var Browser = class {
|
|
|
1110
1209
|
const signal = AbortSignal.any([controller.signal, this.#signal()]);
|
|
1111
1210
|
const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
|
|
1112
1211
|
const exited = once(process, "exit", { signal }).then((values) => {
|
|
1113
|
-
const code =
|
|
1114
|
-
const exitSignal =
|
|
1212
|
+
const code = isInteger(values[0]) ? values[0] : null;
|
|
1213
|
+
const exitSignal = isString(values[1]) ? values[1] : null;
|
|
1115
1214
|
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1116
1215
|
});
|
|
1117
1216
|
try {
|
|
1118
1217
|
return await Promise.race([ready, exited]);
|
|
1119
1218
|
} catch (error) {
|
|
1120
|
-
if (error
|
|
1219
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
1121
1220
|
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
|
|
1122
|
-
throw new BrowserConnectionError(error
|
|
1221
|
+
throw new BrowserConnectionError(isError(error) ? error.message : String(error), context);
|
|
1123
1222
|
} finally {
|
|
1124
1223
|
controller.abort();
|
|
1125
1224
|
ready.catch(() => void 0);
|
|
@@ -1144,7 +1243,7 @@ var Browser = class {
|
|
|
1144
1243
|
} catch {
|
|
1145
1244
|
return;
|
|
1146
1245
|
}
|
|
1147
|
-
if (!isRecord(result) || !
|
|
1246
|
+
if (!isRecord(result) || !isArray(result["targetInfos"])) return;
|
|
1148
1247
|
const pages = [];
|
|
1149
1248
|
for (const target of result["targetInfos"]) {
|
|
1150
1249
|
if (!isRecord(target) || !isString(target["targetId"]) || target["type"] !== "page" || !isString(target["title"]) || !isString(target["url"])) continue;
|
|
@@ -1158,9 +1257,10 @@ var Browser = class {
|
|
|
1158
1257
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
1159
1258
|
const context = new BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
1160
1259
|
await context.sync(pages);
|
|
1161
|
-
this.#
|
|
1260
|
+
this.#registerContext(context);
|
|
1162
1261
|
}
|
|
1163
1262
|
async #destroyResources() {
|
|
1263
|
+
let terminated = false;
|
|
1164
1264
|
try {
|
|
1165
1265
|
await this.#settle();
|
|
1166
1266
|
this.#unbindTransport();
|
|
@@ -1173,13 +1273,22 @@ var Browser = class {
|
|
|
1173
1273
|
this.#contexts = [];
|
|
1174
1274
|
await this.#terminate(this.#process);
|
|
1175
1275
|
this.#process = void 0;
|
|
1176
|
-
|
|
1177
|
-
this.#client = void 0;
|
|
1276
|
+
terminated = true;
|
|
1178
1277
|
} finally {
|
|
1179
|
-
this.#
|
|
1278
|
+
const contexts = this.#contexts;
|
|
1279
|
+
this.#contexts = [];
|
|
1280
|
+
const client = this.#client;
|
|
1281
|
+
this.#client = void 0;
|
|
1282
|
+
try {
|
|
1283
|
+
await this.#cleanupLocal(contexts, client);
|
|
1284
|
+
if (terminated) await this.#releaseProfile();
|
|
1285
|
+
} finally {
|
|
1286
|
+
this.#finish();
|
|
1287
|
+
}
|
|
1180
1288
|
}
|
|
1181
1289
|
}
|
|
1182
1290
|
async #closeResources() {
|
|
1291
|
+
let terminated = false;
|
|
1183
1292
|
try {
|
|
1184
1293
|
await this.#settle();
|
|
1185
1294
|
this.#unbindTransport();
|
|
@@ -1190,18 +1299,33 @@ var Browser = class {
|
|
|
1190
1299
|
await this.#closeRemote(client);
|
|
1191
1300
|
const process = this.#process;
|
|
1192
1301
|
if (process !== void 0) {
|
|
1193
|
-
if (!await this.#
|
|
1302
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1194
1303
|
}
|
|
1195
1304
|
this.#process = void 0;
|
|
1196
|
-
|
|
1197
|
-
this.#client = void 0;
|
|
1305
|
+
terminated = true;
|
|
1198
1306
|
} finally {
|
|
1199
|
-
this.#
|
|
1307
|
+
const contexts = this.#contexts;
|
|
1308
|
+
this.#contexts = [];
|
|
1309
|
+
const client = this.#client;
|
|
1310
|
+
this.#client = void 0;
|
|
1311
|
+
try {
|
|
1312
|
+
await this.#cleanupLocal(contexts, client);
|
|
1313
|
+
if (terminated) await this.#releaseProfile();
|
|
1314
|
+
} finally {
|
|
1315
|
+
this.#finish();
|
|
1316
|
+
}
|
|
1200
1317
|
}
|
|
1201
1318
|
}
|
|
1202
1319
|
async #settle() {
|
|
1203
1320
|
await this.#connecting?.catch(() => void 0);
|
|
1204
1321
|
await this.#disconnecting?.catch(() => void 0);
|
|
1322
|
+
await this.#settleExit();
|
|
1323
|
+
}
|
|
1324
|
+
async #settleExit() {
|
|
1325
|
+
const cleanup = this.#exitCleanup;
|
|
1326
|
+
if (cleanup === void 0) return;
|
|
1327
|
+
await cleanup;
|
|
1328
|
+
if (this.#exitCleanup === cleanup) this.#exitCleanup = void 0;
|
|
1205
1329
|
}
|
|
1206
1330
|
async #closeRemote(client) {
|
|
1207
1331
|
let remote = client;
|
|
@@ -1228,12 +1352,20 @@ var Browser = class {
|
|
|
1228
1352
|
if (temporary) await this.#closeClient(remote);
|
|
1229
1353
|
}
|
|
1230
1354
|
async #closeContexts() {
|
|
1231
|
-
for (const context of this.#contexts) try {
|
|
1355
|
+
for (const context of [...this.#contexts]) try {
|
|
1232
1356
|
await context.close();
|
|
1233
1357
|
} catch {}
|
|
1234
1358
|
}
|
|
1235
1359
|
async #destroyContexts() {
|
|
1236
|
-
await this.#destroyContextList(this.#contexts);
|
|
1360
|
+
await this.#destroyContextList([...this.#contexts]);
|
|
1361
|
+
}
|
|
1362
|
+
#registerContext(context) {
|
|
1363
|
+
this.#contexts.push(context);
|
|
1364
|
+
context.emitter.on("close", () => {
|
|
1365
|
+
const index = this.#contexts.indexOf(context);
|
|
1366
|
+
if (index >= 0) this.#contexts.splice(index, 1);
|
|
1367
|
+
});
|
|
1368
|
+
this.#emitter.emit("context", context);
|
|
1237
1369
|
}
|
|
1238
1370
|
async #destroyContextList(contexts) {
|
|
1239
1371
|
for (const context of contexts) try {
|
|
@@ -1246,9 +1378,24 @@ var Browser = class {
|
|
|
1246
1378
|
await client.close();
|
|
1247
1379
|
} catch {}
|
|
1248
1380
|
}
|
|
1249
|
-
async #
|
|
1250
|
-
|
|
1251
|
-
|
|
1381
|
+
async #releaseProfile() {
|
|
1382
|
+
const profile = this.#profile;
|
|
1383
|
+
this.#profile = void 0;
|
|
1384
|
+
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1385
|
+
}
|
|
1386
|
+
async #cleanupExitedProcess(process, contexts, client) {
|
|
1387
|
+
let terminated = false;
|
|
1388
|
+
try {
|
|
1389
|
+
await this.#terminate(process);
|
|
1390
|
+
terminated = true;
|
|
1391
|
+
} finally {
|
|
1392
|
+
await this.#cleanupLocal(contexts, client);
|
|
1393
|
+
}
|
|
1394
|
+
if (terminated) await this.#releaseProfile();
|
|
1395
|
+
}
|
|
1396
|
+
async #cleanupLocal(contexts, client) {
|
|
1397
|
+
await this.#destroyContextList(contexts);
|
|
1398
|
+
await this.#closeClient(client);
|
|
1252
1399
|
}
|
|
1253
1400
|
async #waitForProcessWithin(process, timeout) {
|
|
1254
1401
|
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
@@ -1261,20 +1408,55 @@ var Browser = class {
|
|
|
1261
1408
|
throw error;
|
|
1262
1409
|
}
|
|
1263
1410
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1411
|
+
#hasProcessGroup(process) {
|
|
1412
|
+
const pid = process.pid;
|
|
1413
|
+
return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
|
|
1414
|
+
}
|
|
1415
|
+
#inspectProcessGroup(process) {
|
|
1416
|
+
if (!this.#hasProcessGroup(process)) return false;
|
|
1417
|
+
return this.#signalProcess(process, 0);
|
|
1418
|
+
}
|
|
1419
|
+
#signalProcess(process, signal) {
|
|
1420
|
+
const pid = process.pid;
|
|
1266
1421
|
try {
|
|
1267
|
-
process.kill(
|
|
1268
|
-
|
|
1269
|
-
|
|
1422
|
+
if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
|
|
1423
|
+
return process.kill(signal);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
const code = isError(error) && "code" in error && isString(error.code) ? error.code : void 0;
|
|
1426
|
+
if (code === "ESRCH") return false;
|
|
1427
|
+
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
1428
|
+
throw new BrowserConnectionError("Failed to signal the browser process", {
|
|
1429
|
+
pid,
|
|
1430
|
+
signal,
|
|
1431
|
+
cause: error
|
|
1432
|
+
});
|
|
1270
1433
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1434
|
+
}
|
|
1435
|
+
async #waitForProcessGroupWithin(process, timeout) {
|
|
1436
|
+
const deadline = performance.now() + timeout;
|
|
1437
|
+
let confirmed = false;
|
|
1438
|
+
while (true) {
|
|
1439
|
+
const alive = this.#inspectProcessGroup(process);
|
|
1440
|
+
if (alive === false) return true;
|
|
1441
|
+
if (alive === true) confirmed = true;
|
|
1442
|
+
const remaining = deadline - performance.now();
|
|
1443
|
+
if (remaining <= 0) return confirmed ? false : void 0;
|
|
1444
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(BROWSER_WAIT_POLL_INTERVAL_MS, remaining)));
|
|
1276
1445
|
}
|
|
1277
|
-
|
|
1446
|
+
}
|
|
1447
|
+
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
1448
|
+
const exited = this.#waitForProcessWithin(process, timeout);
|
|
1449
|
+
const drained = this.#waitForProcessGroupWithin(process, timeout);
|
|
1450
|
+
const results = await Promise.all([exited, drained]);
|
|
1451
|
+
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
1452
|
+
}
|
|
1453
|
+
async #terminate(process) {
|
|
1454
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectProcessGroup(process) === false) return;
|
|
1455
|
+
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
1456
|
+
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
1457
|
+
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
1458
|
+
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
1459
|
+
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
|
|
1278
1460
|
}
|
|
1279
1461
|
#finish() {
|
|
1280
1462
|
this.#unbindTransport();
|
|
@@ -1285,12 +1467,13 @@ var Browser = class {
|
|
|
1285
1467
|
this.#endpoint = void 0;
|
|
1286
1468
|
this.#client = void 0;
|
|
1287
1469
|
this.#process = void 0;
|
|
1470
|
+
this.#exitCleanup = void 0;
|
|
1288
1471
|
this.#contexts = [];
|
|
1289
1472
|
this.#emitter.emit("destroy");
|
|
1290
1473
|
this.#emitter.destroy();
|
|
1291
1474
|
}
|
|
1292
1475
|
};
|
|
1293
1476
|
//#endregion
|
|
1294
|
-
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_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, createCDPTransport, createScreenshotWriter, defaultInstallPaths, defaultStoreBases, fetchCDPTargets, findAllEnvOverrides, findAllInStore, findAllInstallPaths, findEnvOverride, findInStore, findInstallPath, findSystemBrowser, findSystemBrowsers, isBrowserConnectionError, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, probeAllPathNames, probePathNames, waitForCDPReady, windowsRoots };
|
|
1477
|
+
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 };
|
|
1295
1478
|
|
|
1296
1479
|
//# sourceMappingURL=index.js.map
|