@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
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _src_core = require("../core/index.cjs");
|
|
3
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
4
|
let node_fs = require("node:fs");
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
6
|
+
let node_os = require("node:os");
|
|
4
7
|
let node_path = require("node:path");
|
|
5
8
|
let node_child_process = require("node:child_process");
|
|
6
9
|
let node_timers_promises = require("node:timers/promises");
|
|
7
|
-
let _orkestrel_contract = require("@orkestrel/contract");
|
|
8
10
|
let node_crypto = require("node:crypto");
|
|
9
11
|
let node_http = require("node:http");
|
|
10
12
|
let node_https = require("node:https");
|
|
11
13
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
12
14
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
13
15
|
let node_events = require("node:events");
|
|
14
|
-
let node_fs_promises = require("node:fs/promises");
|
|
15
16
|
//#region src/server/constants.ts
|
|
16
17
|
/** Default CDP port probed for an existing browser and used for launches. */
|
|
17
18
|
var BROWSER_DEFAULT_CDP_PORT = 9222;
|
|
@@ -27,7 +28,9 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
|
27
28
|
var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
|
|
28
29
|
/** Flag enabling headless mode on a launched browser process. */
|
|
29
30
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
30
|
-
/**
|
|
31
|
+
/** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
32
|
+
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
33
|
+
/** Bound for each launched-process exit window during TERM-to-KILL teardown. */
|
|
31
34
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
32
35
|
/** Bound for the `discover: false` port-occupancy probe before launching — short, since it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
|
|
33
36
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
@@ -158,7 +161,7 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
|
|
|
158
161
|
* @returns True when value is a BrowserConnectionError instance
|
|
159
162
|
*/
|
|
160
163
|
function isBrowserConnectionError(value) {
|
|
161
|
-
return value
|
|
164
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
|
|
162
165
|
}
|
|
163
166
|
/**
|
|
164
167
|
* Narrow an unknown value to BrowserNotConnectedError.
|
|
@@ -167,7 +170,7 @@ function isBrowserConnectionError(value) {
|
|
|
167
170
|
* @returns True when value is a BrowserNotConnectedError instance
|
|
168
171
|
*/
|
|
169
172
|
function isBrowserNotConnectedError(value) {
|
|
170
|
-
return value
|
|
173
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
|
|
171
174
|
}
|
|
172
175
|
/**
|
|
173
176
|
* Narrow an unknown value to BrowserDestroyedError.
|
|
@@ -176,7 +179,7 @@ function isBrowserNotConnectedError(value) {
|
|
|
176
179
|
* @returns True when value is a BrowserDestroyedError instance
|
|
177
180
|
*/
|
|
178
181
|
function isBrowserDestroyedError(value) {
|
|
179
|
-
return value
|
|
182
|
+
return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
|
|
180
183
|
}
|
|
181
184
|
//#endregion
|
|
182
185
|
//#region src/server/helpers.ts
|
|
@@ -264,6 +267,44 @@ function browserToEngine(browser) {
|
|
|
264
267
|
if (browser?.includes("Chrome/") === true) return "chrome";
|
|
265
268
|
return "chromium";
|
|
266
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a persistent caller profile or create an isolated temporary one.
|
|
272
|
+
*
|
|
273
|
+
* @param path - Optional caller-owned persistent user-data directory
|
|
274
|
+
* @returns The resolved profile and whether the library owns its lifecycle
|
|
275
|
+
*/
|
|
276
|
+
async function createBrowserProfile(path) {
|
|
277
|
+
if (path !== void 0) return {
|
|
278
|
+
path,
|
|
279
|
+
temporary: false
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
path: await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), BROWSER_PROFILE_PREFIX)),
|
|
283
|
+
temporary: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Remove a library-owned isolated browser profile.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* The path must remain a direct child of the operating-system temp directory
|
|
291
|
+
* with {@link BROWSER_PROFILE_PREFIX}; this defensive check prevents a
|
|
292
|
+
* malformed profile value from widening recursive deletion.
|
|
293
|
+
*
|
|
294
|
+
* @param profile - Resolved profile lifecycle result
|
|
295
|
+
*/
|
|
296
|
+
async function removeBrowserProfile(profile) {
|
|
297
|
+
if (!profile.temporary) return;
|
|
298
|
+
const path = (0, node_path.resolve)(profile.path);
|
|
299
|
+
const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
|
|
300
|
+
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new Error("Refusing to remove an unsafe browser profile path");
|
|
301
|
+
await (0, node_fs_promises.rm)(path, {
|
|
302
|
+
recursive: true,
|
|
303
|
+
force: true,
|
|
304
|
+
maxRetries: 3,
|
|
305
|
+
retryDelay: 100
|
|
306
|
+
});
|
|
307
|
+
}
|
|
267
308
|
/** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
|
|
268
309
|
function findEnvOverride(env) {
|
|
269
310
|
return findAllEnvOverrides(env)[0];
|
|
@@ -354,6 +395,10 @@ function findAllInStore(base, platform) {
|
|
|
354
395
|
/**
|
|
355
396
|
* Launch a browser process with raw-CDP debugging flags.
|
|
356
397
|
*
|
|
398
|
+
* @remarks
|
|
399
|
+
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
400
|
+
* signal and await every Chromium subprocess without affecting the caller.
|
|
401
|
+
*
|
|
357
402
|
* @param executable - Absolute path to the browser executable
|
|
358
403
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
359
404
|
* @param headless - Whether to launch in headless mode
|
|
@@ -367,7 +412,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
367
412
|
args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
|
|
368
413
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
369
414
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
370
|
-
return (0, node_child_process.spawn)(executable, args, {
|
|
415
|
+
return (0, node_child_process.spawn)(executable, args, {
|
|
416
|
+
stdio: "ignore",
|
|
417
|
+
detached: process.platform !== "win32"
|
|
418
|
+
});
|
|
371
419
|
}
|
|
372
420
|
/**
|
|
373
421
|
* Poll a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
@@ -416,7 +464,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
416
464
|
const response = await fetch(url, { signal: controller.signal });
|
|
417
465
|
if (!response.ok) return [];
|
|
418
466
|
const list = await response.json();
|
|
419
|
-
if (!
|
|
467
|
+
if (!(0, _orkestrel_contract.isArray)(list)) return [];
|
|
420
468
|
const targets = [];
|
|
421
469
|
for (const entry of list) {
|
|
422
470
|
if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
|
|
@@ -458,11 +506,11 @@ var WebSocketCDPTransport = class {
|
|
|
458
506
|
#reject;
|
|
459
507
|
#timer;
|
|
460
508
|
#closeResolve;
|
|
461
|
-
#onSocketClose =
|
|
509
|
+
#onSocketClose = this.#handleSocketClose.bind(this);
|
|
462
510
|
constructor(options) {
|
|
463
511
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
464
|
-
on: options.on,
|
|
465
|
-
error: options.error
|
|
512
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
513
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
466
514
|
});
|
|
467
515
|
this.#url = options.url;
|
|
468
516
|
this.#timeout = options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -480,12 +528,12 @@ var WebSocketCDPTransport = class {
|
|
|
480
528
|
}
|
|
481
529
|
this.#socket?.destroy();
|
|
482
530
|
this.#socket = void 0;
|
|
483
|
-
const
|
|
484
|
-
this.#starting =
|
|
531
|
+
const transition = this.#open();
|
|
532
|
+
this.#starting = transition;
|
|
485
533
|
try {
|
|
486
|
-
await
|
|
534
|
+
await transition;
|
|
487
535
|
} finally {
|
|
488
|
-
if (this.#starting ===
|
|
536
|
+
if (this.#starting === transition) this.#starting = void 0;
|
|
489
537
|
}
|
|
490
538
|
}
|
|
491
539
|
async send(data) {
|
|
@@ -499,16 +547,21 @@ var WebSocketCDPTransport = class {
|
|
|
499
547
|
await active;
|
|
500
548
|
return;
|
|
501
549
|
}
|
|
502
|
-
const
|
|
503
|
-
this.#closing =
|
|
550
|
+
const transition = this.#stop();
|
|
551
|
+
this.#closing = transition;
|
|
504
552
|
try {
|
|
505
|
-
await
|
|
553
|
+
await transition;
|
|
506
554
|
} finally {
|
|
507
|
-
if (this.#closing ===
|
|
555
|
+
if (this.#closing === transition) this.#closing = void 0;
|
|
508
556
|
}
|
|
509
557
|
}
|
|
510
558
|
async #open() {
|
|
511
|
-
const
|
|
559
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
|
|
560
|
+
if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
561
|
+
url: this.#url,
|
|
562
|
+
error: parsed.error
|
|
563
|
+
});
|
|
564
|
+
const url = parsed.value;
|
|
512
565
|
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
513
566
|
const key = (0, node_crypto.randomBytes)(16).toString("base64");
|
|
514
567
|
const deferred = Promise.withResolvers();
|
|
@@ -628,6 +681,9 @@ var WebSocketCDPTransport = class {
|
|
|
628
681
|
});
|
|
629
682
|
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
630
683
|
}
|
|
684
|
+
#handleSocketClose() {
|
|
685
|
+
this.#closeResolve?.();
|
|
686
|
+
}
|
|
631
687
|
};
|
|
632
688
|
//#endregion
|
|
633
689
|
//#region src/server/factories.ts
|
|
@@ -670,6 +726,9 @@ function createScreenshotWriter() {
|
|
|
670
726
|
}
|
|
671
727
|
//#endregion
|
|
672
728
|
//#region src/server/Browser.ts
|
|
729
|
+
/**
|
|
730
|
+
* Discovers, launches, connects to, and owns Chromium-family browser sessions.
|
|
731
|
+
*/
|
|
673
732
|
var Browser = class {
|
|
674
733
|
#emitter;
|
|
675
734
|
#options;
|
|
@@ -683,19 +742,21 @@ var Browser = class {
|
|
|
683
742
|
#endpoint;
|
|
684
743
|
#client;
|
|
685
744
|
#process;
|
|
745
|
+
#profile;
|
|
686
746
|
#contexts = [];
|
|
687
747
|
#destroyed = false;
|
|
688
748
|
#connecting;
|
|
689
749
|
#disconnecting;
|
|
750
|
+
#exitCleanup;
|
|
690
751
|
#shutdown;
|
|
691
752
|
#transport;
|
|
692
|
-
#onTransportClose =
|
|
693
|
-
#onTransportError =
|
|
694
|
-
#onProcessExit =
|
|
753
|
+
#onTransportClose = this.#handleTransportLoss.bind(this);
|
|
754
|
+
#onTransportError = this.#handleTransportLoss.bind(this);
|
|
755
|
+
#onProcessExit = this.#handleProcessExit.bind(this);
|
|
695
756
|
constructor(options) {
|
|
696
757
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
697
|
-
on: options
|
|
698
|
-
error: options
|
|
758
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
759
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
699
760
|
});
|
|
700
761
|
this.#options = options ?? {};
|
|
701
762
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
@@ -731,14 +792,12 @@ var Browser = class {
|
|
|
731
792
|
}
|
|
732
793
|
async connect() {
|
|
733
794
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
734
|
-
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
735
|
-
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
736
|
-
if (this.#status === "connected") return;
|
|
737
795
|
const active = this.#connecting;
|
|
738
796
|
if (active !== void 0) {
|
|
739
797
|
await active;
|
|
740
798
|
return;
|
|
741
799
|
}
|
|
800
|
+
if (this.#status === "connected") return;
|
|
742
801
|
const attempt = this.#establish();
|
|
743
802
|
this.#connecting = attempt;
|
|
744
803
|
try {
|
|
@@ -754,15 +813,11 @@ var Browser = class {
|
|
|
754
813
|
}
|
|
755
814
|
async disconnect() {
|
|
756
815
|
if (this.#destroyed) return;
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (this.#connecting !== void 0) {
|
|
763
|
-
await this.#connecting.catch(() => void 0);
|
|
764
|
-
if (this.#destroyed) return;
|
|
765
|
-
}
|
|
816
|
+
const disconnecting = this.#disconnecting;
|
|
817
|
+
const connecting = this.#connecting;
|
|
818
|
+
if (disconnecting !== void 0) await disconnecting;
|
|
819
|
+
if (connecting !== void 0) await connecting.catch(() => void 0);
|
|
820
|
+
if (this.#destroyed) return;
|
|
766
821
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
767
822
|
const attempt = this.#detach();
|
|
768
823
|
this.#disconnecting = attempt;
|
|
@@ -780,6 +835,35 @@ var Browser = class {
|
|
|
780
835
|
contexts() {
|
|
781
836
|
return [...this.#contexts];
|
|
782
837
|
}
|
|
838
|
+
async isolate(options) {
|
|
839
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
840
|
+
const client = this.#client;
|
|
841
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
842
|
+
(0, _src_core.validateBrowserContextOptions)(options);
|
|
843
|
+
const params = { disposeOnDetach: false };
|
|
844
|
+
if (options?.proxy !== void 0) {
|
|
845
|
+
params["proxyServer"] = options.proxy.server;
|
|
846
|
+
if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
|
|
847
|
+
}
|
|
848
|
+
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
849
|
+
const result = await client.send("Target.createBrowserContext", params);
|
|
850
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
|
|
851
|
+
const id = result["browserContextId"];
|
|
852
|
+
const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
|
|
853
|
+
try {
|
|
854
|
+
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
855
|
+
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
856
|
+
browserContextId: id,
|
|
857
|
+
downloadPath: options.downloads.path,
|
|
858
|
+
eventsEnabled: true
|
|
859
|
+
});
|
|
860
|
+
} catch (error) {
|
|
861
|
+
await context.close().catch(() => void 0);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
this.#registerContext(context);
|
|
865
|
+
return context;
|
|
866
|
+
}
|
|
783
867
|
async create(options) {
|
|
784
868
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
785
869
|
const client = this.#client;
|
|
@@ -787,7 +871,7 @@ var Browser = class {
|
|
|
787
871
|
let context = this.#contexts[0];
|
|
788
872
|
if (context === void 0) {
|
|
789
873
|
context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
790
|
-
this.#
|
|
874
|
+
this.#registerContext(context);
|
|
791
875
|
}
|
|
792
876
|
const page = await context.create(options);
|
|
793
877
|
this.#emitter.emit("page", page);
|
|
@@ -814,6 +898,10 @@ var Browser = class {
|
|
|
814
898
|
return shutdown;
|
|
815
899
|
}
|
|
816
900
|
async #establish() {
|
|
901
|
+
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
902
|
+
await this.#settleExit();
|
|
903
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
904
|
+
if (this.#status === "connected") return;
|
|
817
905
|
this.#assertNotAborted();
|
|
818
906
|
this.#status = "connecting";
|
|
819
907
|
try {
|
|
@@ -840,8 +928,8 @@ var Browser = class {
|
|
|
840
928
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
841
929
|
this.#status = "error";
|
|
842
930
|
this.#emitter.emit("error", error);
|
|
843
|
-
if (error
|
|
844
|
-
throw new BrowserConnectionError(error
|
|
931
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
932
|
+
throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), { executable: this.#options.executable });
|
|
845
933
|
}
|
|
846
934
|
}
|
|
847
935
|
async #detach() {
|
|
@@ -887,17 +975,19 @@ var Browser = class {
|
|
|
887
975
|
const connected = this.#status === "connected";
|
|
888
976
|
const client = this.#client;
|
|
889
977
|
const contexts = this.#contexts;
|
|
978
|
+
const process = this.#process;
|
|
890
979
|
this.#unbindTransport();
|
|
891
980
|
this.#unbindProcess();
|
|
892
981
|
this.#process = void 0;
|
|
982
|
+
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
983
|
+
this.#exitCleanup = cleanup;
|
|
984
|
+
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
893
985
|
this.#client = void 0;
|
|
894
986
|
this.#contexts = [];
|
|
895
987
|
this.#connection = void 0;
|
|
896
988
|
this.#owned = void 0;
|
|
897
989
|
this.#endpoint = void 0;
|
|
898
990
|
if (connected) this.#status = "disconnected";
|
|
899
|
-
this.#destroyContextList(contexts);
|
|
900
|
-
this.#closeClient(client);
|
|
901
991
|
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
902
992
|
if (connected) {
|
|
903
993
|
this.#emitter.emit("disconnect");
|
|
@@ -1057,15 +1147,23 @@ var Browser = class {
|
|
|
1057
1147
|
else {
|
|
1058
1148
|
const found = findSystemBrowser({
|
|
1059
1149
|
...this.#options.browsers,
|
|
1060
|
-
engine: requestedEngine
|
|
1150
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
1061
1151
|
});
|
|
1062
1152
|
executable = found?.executable;
|
|
1063
1153
|
resolvedEngine = found?.engine;
|
|
1064
1154
|
}
|
|
1065
1155
|
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
1066
1156
|
this.#engine = resolvedEngine ?? "chromium";
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1157
|
+
await this.#releaseProfile();
|
|
1158
|
+
const profile = await createBrowserProfile(this.#options.profile);
|
|
1159
|
+
this.#profile = profile;
|
|
1160
|
+
let process;
|
|
1161
|
+
try {
|
|
1162
|
+
process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
await this.#releaseProfile();
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1069
1167
|
this.#process = process;
|
|
1070
1168
|
let client;
|
|
1071
1169
|
try {
|
|
@@ -1079,7 +1177,7 @@ var Browser = class {
|
|
|
1079
1177
|
timeout: this.#timeout()
|
|
1080
1178
|
});
|
|
1081
1179
|
await this.#raceAbort(client.connect());
|
|
1082
|
-
const connection = profile === void 0 ? "launch" : "persistent";
|
|
1180
|
+
const connection = this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1083
1181
|
this.#client = client;
|
|
1084
1182
|
this.#endpoint = endpoint;
|
|
1085
1183
|
this.#connection = connection;
|
|
@@ -1095,6 +1193,7 @@ var Browser = class {
|
|
|
1095
1193
|
this.#unbindProcess();
|
|
1096
1194
|
await this.#closeClient(client);
|
|
1097
1195
|
await this.#terminate(process);
|
|
1196
|
+
await this.#releaseProfile();
|
|
1098
1197
|
this.#process = void 0;
|
|
1099
1198
|
this.#client = void 0;
|
|
1100
1199
|
this.#endpoint = void 0;
|
|
@@ -1111,16 +1210,16 @@ var Browser = class {
|
|
|
1111
1210
|
const signal = AbortSignal.any([controller.signal, this.#signal()]);
|
|
1112
1211
|
const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
|
|
1113
1212
|
const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
|
|
1114
|
-
const code =
|
|
1115
|
-
const exitSignal =
|
|
1213
|
+
const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
|
|
1214
|
+
const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
|
|
1116
1215
|
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1117
1216
|
});
|
|
1118
1217
|
try {
|
|
1119
1218
|
return await Promise.race([ready, exited]);
|
|
1120
1219
|
} catch (error) {
|
|
1121
|
-
if (error
|
|
1220
|
+
if (isBrowserConnectionError(error)) throw error;
|
|
1122
1221
|
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
|
|
1123
|
-
throw new BrowserConnectionError(error
|
|
1222
|
+
throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), context);
|
|
1124
1223
|
} finally {
|
|
1125
1224
|
controller.abort();
|
|
1126
1225
|
ready.catch(() => void 0);
|
|
@@ -1145,7 +1244,7 @@ var Browser = class {
|
|
|
1145
1244
|
} catch {
|
|
1146
1245
|
return;
|
|
1147
1246
|
}
|
|
1148
|
-
if (!(0, _orkestrel_contract.isRecord)(result) || !
|
|
1247
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
|
|
1149
1248
|
const pages = [];
|
|
1150
1249
|
for (const target of result["targetInfos"]) {
|
|
1151
1250
|
if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || target["type"] !== "page" || !(0, _orkestrel_contract.isString)(target["title"]) || !(0, _orkestrel_contract.isString)(target["url"])) continue;
|
|
@@ -1159,9 +1258,10 @@ var Browser = class {
|
|
|
1159
1258
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
1160
1259
|
const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
1161
1260
|
await context.sync(pages);
|
|
1162
|
-
this.#
|
|
1261
|
+
this.#registerContext(context);
|
|
1163
1262
|
}
|
|
1164
1263
|
async #destroyResources() {
|
|
1264
|
+
let terminated = false;
|
|
1165
1265
|
try {
|
|
1166
1266
|
await this.#settle();
|
|
1167
1267
|
this.#unbindTransport();
|
|
@@ -1174,13 +1274,22 @@ var Browser = class {
|
|
|
1174
1274
|
this.#contexts = [];
|
|
1175
1275
|
await this.#terminate(this.#process);
|
|
1176
1276
|
this.#process = void 0;
|
|
1177
|
-
|
|
1178
|
-
this.#client = void 0;
|
|
1277
|
+
terminated = true;
|
|
1179
1278
|
} finally {
|
|
1180
|
-
this.#
|
|
1279
|
+
const contexts = this.#contexts;
|
|
1280
|
+
this.#contexts = [];
|
|
1281
|
+
const client = this.#client;
|
|
1282
|
+
this.#client = void 0;
|
|
1283
|
+
try {
|
|
1284
|
+
await this.#cleanupLocal(contexts, client);
|
|
1285
|
+
if (terminated) await this.#releaseProfile();
|
|
1286
|
+
} finally {
|
|
1287
|
+
this.#finish();
|
|
1288
|
+
}
|
|
1181
1289
|
}
|
|
1182
1290
|
}
|
|
1183
1291
|
async #closeResources() {
|
|
1292
|
+
let terminated = false;
|
|
1184
1293
|
try {
|
|
1185
1294
|
await this.#settle();
|
|
1186
1295
|
this.#unbindTransport();
|
|
@@ -1191,18 +1300,33 @@ var Browser = class {
|
|
|
1191
1300
|
await this.#closeRemote(client);
|
|
1192
1301
|
const process = this.#process;
|
|
1193
1302
|
if (process !== void 0) {
|
|
1194
|
-
if (!await this.#
|
|
1303
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1195
1304
|
}
|
|
1196
1305
|
this.#process = void 0;
|
|
1197
|
-
|
|
1198
|
-
this.#client = void 0;
|
|
1306
|
+
terminated = true;
|
|
1199
1307
|
} finally {
|
|
1200
|
-
this.#
|
|
1308
|
+
const contexts = this.#contexts;
|
|
1309
|
+
this.#contexts = [];
|
|
1310
|
+
const client = this.#client;
|
|
1311
|
+
this.#client = void 0;
|
|
1312
|
+
try {
|
|
1313
|
+
await this.#cleanupLocal(contexts, client);
|
|
1314
|
+
if (terminated) await this.#releaseProfile();
|
|
1315
|
+
} finally {
|
|
1316
|
+
this.#finish();
|
|
1317
|
+
}
|
|
1201
1318
|
}
|
|
1202
1319
|
}
|
|
1203
1320
|
async #settle() {
|
|
1204
1321
|
await this.#connecting?.catch(() => void 0);
|
|
1205
1322
|
await this.#disconnecting?.catch(() => void 0);
|
|
1323
|
+
await this.#settleExit();
|
|
1324
|
+
}
|
|
1325
|
+
async #settleExit() {
|
|
1326
|
+
const cleanup = this.#exitCleanup;
|
|
1327
|
+
if (cleanup === void 0) return;
|
|
1328
|
+
await cleanup;
|
|
1329
|
+
if (this.#exitCleanup === cleanup) this.#exitCleanup = void 0;
|
|
1206
1330
|
}
|
|
1207
1331
|
async #closeRemote(client) {
|
|
1208
1332
|
let remote = client;
|
|
@@ -1229,12 +1353,20 @@ var Browser = class {
|
|
|
1229
1353
|
if (temporary) await this.#closeClient(remote);
|
|
1230
1354
|
}
|
|
1231
1355
|
async #closeContexts() {
|
|
1232
|
-
for (const context of this.#contexts) try {
|
|
1356
|
+
for (const context of [...this.#contexts]) try {
|
|
1233
1357
|
await context.close();
|
|
1234
1358
|
} catch {}
|
|
1235
1359
|
}
|
|
1236
1360
|
async #destroyContexts() {
|
|
1237
|
-
await this.#destroyContextList(this.#contexts);
|
|
1361
|
+
await this.#destroyContextList([...this.#contexts]);
|
|
1362
|
+
}
|
|
1363
|
+
#registerContext(context) {
|
|
1364
|
+
this.#contexts.push(context);
|
|
1365
|
+
context.emitter.on("close", () => {
|
|
1366
|
+
const index = this.#contexts.indexOf(context);
|
|
1367
|
+
if (index >= 0) this.#contexts.splice(index, 1);
|
|
1368
|
+
});
|
|
1369
|
+
this.#emitter.emit("context", context);
|
|
1238
1370
|
}
|
|
1239
1371
|
async #destroyContextList(contexts) {
|
|
1240
1372
|
for (const context of contexts) try {
|
|
@@ -1247,9 +1379,24 @@ var Browser = class {
|
|
|
1247
1379
|
await client.close();
|
|
1248
1380
|
} catch {}
|
|
1249
1381
|
}
|
|
1250
|
-
async #
|
|
1251
|
-
|
|
1252
|
-
|
|
1382
|
+
async #releaseProfile() {
|
|
1383
|
+
const profile = this.#profile;
|
|
1384
|
+
this.#profile = void 0;
|
|
1385
|
+
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1386
|
+
}
|
|
1387
|
+
async #cleanupExitedProcess(process, contexts, client) {
|
|
1388
|
+
let terminated = false;
|
|
1389
|
+
try {
|
|
1390
|
+
await this.#terminate(process);
|
|
1391
|
+
terminated = true;
|
|
1392
|
+
} finally {
|
|
1393
|
+
await this.#cleanupLocal(contexts, client);
|
|
1394
|
+
}
|
|
1395
|
+
if (terminated) await this.#releaseProfile();
|
|
1396
|
+
}
|
|
1397
|
+
async #cleanupLocal(contexts, client) {
|
|
1398
|
+
await this.#destroyContextList(contexts);
|
|
1399
|
+
await this.#closeClient(client);
|
|
1253
1400
|
}
|
|
1254
1401
|
async #waitForProcessWithin(process, timeout) {
|
|
1255
1402
|
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
@@ -1262,20 +1409,55 @@ var Browser = class {
|
|
|
1262
1409
|
throw error;
|
|
1263
1410
|
}
|
|
1264
1411
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1412
|
+
#hasProcessGroup(process) {
|
|
1413
|
+
const pid = process.pid;
|
|
1414
|
+
return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
|
|
1415
|
+
}
|
|
1416
|
+
#inspectProcessGroup(process) {
|
|
1417
|
+
if (!this.#hasProcessGroup(process)) return false;
|
|
1418
|
+
return this.#signalProcess(process, 0);
|
|
1419
|
+
}
|
|
1420
|
+
#signalProcess(process, signal) {
|
|
1421
|
+
const pid = process.pid;
|
|
1267
1422
|
try {
|
|
1268
|
-
process.kill(
|
|
1269
|
-
|
|
1270
|
-
|
|
1423
|
+
if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
|
|
1424
|
+
return process.kill(signal);
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
|
|
1427
|
+
if (code === "ESRCH") return false;
|
|
1428
|
+
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
1429
|
+
throw new BrowserConnectionError("Failed to signal the browser process", {
|
|
1430
|
+
pid,
|
|
1431
|
+
signal,
|
|
1432
|
+
cause: error
|
|
1433
|
+
});
|
|
1271
1434
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1435
|
+
}
|
|
1436
|
+
async #waitForProcessGroupWithin(process, timeout) {
|
|
1437
|
+
const deadline = performance.now() + timeout;
|
|
1438
|
+
let confirmed = false;
|
|
1439
|
+
while (true) {
|
|
1440
|
+
const alive = this.#inspectProcessGroup(process);
|
|
1441
|
+
if (alive === false) return true;
|
|
1442
|
+
if (alive === true) confirmed = true;
|
|
1443
|
+
const remaining = deadline - performance.now();
|
|
1444
|
+
if (remaining <= 0) return confirmed ? false : void 0;
|
|
1445
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, remaining)));
|
|
1277
1446
|
}
|
|
1278
|
-
|
|
1447
|
+
}
|
|
1448
|
+
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
1449
|
+
const exited = this.#waitForProcessWithin(process, timeout);
|
|
1450
|
+
const drained = this.#waitForProcessGroupWithin(process, timeout);
|
|
1451
|
+
const results = await Promise.all([exited, drained]);
|
|
1452
|
+
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
1453
|
+
}
|
|
1454
|
+
async #terminate(process) {
|
|
1455
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectProcessGroup(process) === false) return;
|
|
1456
|
+
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
1457
|
+
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
1458
|
+
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
1459
|
+
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
1460
|
+
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
|
|
1279
1461
|
}
|
|
1280
1462
|
#finish() {
|
|
1281
1463
|
this.#unbindTransport();
|
|
@@ -1286,6 +1468,7 @@ var Browser = class {
|
|
|
1286
1468
|
this.#endpoint = void 0;
|
|
1287
1469
|
this.#client = void 0;
|
|
1288
1470
|
this.#process = void 0;
|
|
1471
|
+
this.#exitCleanup = void 0;
|
|
1289
1472
|
this.#contexts = [];
|
|
1290
1473
|
this.#emitter.emit("destroy");
|
|
1291
1474
|
this.#emitter.destroy();
|
|
@@ -1306,6 +1489,7 @@ exports.BROWSER_KILL_GRACE_MS = BROWSER_KILL_GRACE_MS;
|
|
|
1306
1489
|
exports.BROWSER_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS;
|
|
1307
1490
|
exports.BROWSER_PORT_PROBE_TIMEOUT_MS = BROWSER_PORT_PROBE_TIMEOUT_MS;
|
|
1308
1491
|
exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
|
|
1492
|
+
exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
|
|
1309
1493
|
exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
|
|
1310
1494
|
exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
|
|
1311
1495
|
exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
|
|
@@ -1322,6 +1506,7 @@ exports.BrowserNotConnectedError = BrowserNotConnectedError;
|
|
|
1322
1506
|
exports.WebSocketCDPTransport = WebSocketCDPTransport;
|
|
1323
1507
|
exports.browserToEngine = browserToEngine;
|
|
1324
1508
|
exports.createBrowser = createBrowser;
|
|
1509
|
+
exports.createBrowserProfile = createBrowserProfile;
|
|
1325
1510
|
exports.createCDPTransport = createCDPTransport;
|
|
1326
1511
|
exports.createScreenshotWriter = createScreenshotWriter;
|
|
1327
1512
|
exports.defaultInstallPaths = defaultInstallPaths;
|
|
@@ -1343,6 +1528,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
|
1343
1528
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
1344
1529
|
exports.probeAllPathNames = probeAllPathNames;
|
|
1345
1530
|
exports.probePathNames = probePathNames;
|
|
1531
|
+
exports.removeBrowserProfile = removeBrowserProfile;
|
|
1346
1532
|
exports.waitForCDPReady = waitForCDPReady;
|
|
1347
1533
|
exports.windowsRoots = windowsRoots;
|
|
1348
1534
|
|