@orkestrel/browser 0.0.6 → 0.0.8
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 +4 -5
- package/dist/src/core/index.cjs +564 -661
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +96 -199
- package/dist/src/core/index.d.ts +96 -199
- package/dist/src/core/index.js +563 -643
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +127 -52
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +10 -5
- package/dist/src/server/index.d.ts +10 -5
- package/dist/src/server/index.js +127 -52
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -17
|
@@ -30,7 +30,7 @@ var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser
|
|
|
30
30
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
31
31
|
/** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
32
32
|
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
33
|
-
/**
|
|
33
|
+
/** Bound for each launched-process exit window during TERM-to-KILL teardown. */
|
|
34
34
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
35
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. */
|
|
36
36
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
@@ -387,7 +387,8 @@ function findAllInStore(base, platform) {
|
|
|
387
387
|
if ((0, node_fs.existsSync)(linkPath)) found.push(linkPath);
|
|
388
388
|
const glob = BROWSER_STORE_GLOBS[platform];
|
|
389
389
|
if (glob === void 0) return found;
|
|
390
|
-
const
|
|
390
|
+
const pattern = `${base.replaceAll("\\", "/")}/${glob}`;
|
|
391
|
+
const matches = (0, node_fs.globSync)(pattern).filter((match) => (0, node_fs.existsSync)(match));
|
|
391
392
|
matches.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
392
393
|
found.push(...matches);
|
|
393
394
|
return found;
|
|
@@ -395,6 +396,10 @@ function findAllInStore(base, platform) {
|
|
|
395
396
|
/**
|
|
396
397
|
* Launch a browser process with raw-CDP debugging flags.
|
|
397
398
|
*
|
|
399
|
+
* @remarks
|
|
400
|
+
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
401
|
+
* signal and await every Chromium subprocess without affecting the caller.
|
|
402
|
+
*
|
|
398
403
|
* @param executable - Absolute path to the browser executable
|
|
399
404
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
400
405
|
* @param headless - Whether to launch in headless mode
|
|
@@ -408,7 +413,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
408
413
|
args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
|
|
409
414
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
410
415
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
411
|
-
return (0, node_child_process.spawn)(executable, args, {
|
|
416
|
+
return (0, node_child_process.spawn)(executable, args, {
|
|
417
|
+
stdio: "ignore",
|
|
418
|
+
detached: process.platform !== "win32"
|
|
419
|
+
});
|
|
412
420
|
}
|
|
413
421
|
/**
|
|
414
422
|
* Poll a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
@@ -499,11 +507,11 @@ var WebSocketCDPTransport = class {
|
|
|
499
507
|
#reject;
|
|
500
508
|
#timer;
|
|
501
509
|
#closeResolve;
|
|
502
|
-
#onSocketClose =
|
|
510
|
+
#onSocketClose = this.#handleSocketClose.bind(this);
|
|
503
511
|
constructor(options) {
|
|
504
512
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
505
|
-
on: options.on,
|
|
506
|
-
error: options.error
|
|
513
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
514
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
507
515
|
});
|
|
508
516
|
this.#url = options.url;
|
|
509
517
|
this.#timeout = options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -674,6 +682,9 @@ var WebSocketCDPTransport = class {
|
|
|
674
682
|
});
|
|
675
683
|
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
676
684
|
}
|
|
685
|
+
#handleSocketClose() {
|
|
686
|
+
this.#closeResolve?.();
|
|
687
|
+
}
|
|
677
688
|
};
|
|
678
689
|
//#endregion
|
|
679
690
|
//#region src/server/factories.ts
|
|
@@ -737,15 +748,16 @@ var Browser = class {
|
|
|
737
748
|
#destroyed = false;
|
|
738
749
|
#connecting;
|
|
739
750
|
#disconnecting;
|
|
751
|
+
#exitCleanup;
|
|
740
752
|
#shutdown;
|
|
741
753
|
#transport;
|
|
742
|
-
#onTransportClose =
|
|
743
|
-
#onTransportError =
|
|
744
|
-
#onProcessExit =
|
|
754
|
+
#onTransportClose = this.#handleTransportLoss.bind(this);
|
|
755
|
+
#onTransportError = this.#handleTransportLoss.bind(this);
|
|
756
|
+
#onProcessExit = this.#handleProcessExit.bind(this);
|
|
745
757
|
constructor(options) {
|
|
746
758
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
747
|
-
on: options
|
|
748
|
-
error: options
|
|
759
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
760
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
749
761
|
});
|
|
750
762
|
this.#options = options ?? {};
|
|
751
763
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
@@ -781,14 +793,12 @@ var Browser = class {
|
|
|
781
793
|
}
|
|
782
794
|
async connect() {
|
|
783
795
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
784
|
-
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
785
|
-
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
786
|
-
if (this.#status === "connected") return;
|
|
787
796
|
const active = this.#connecting;
|
|
788
797
|
if (active !== void 0) {
|
|
789
798
|
await active;
|
|
790
799
|
return;
|
|
791
800
|
}
|
|
801
|
+
if (this.#status === "connected") return;
|
|
792
802
|
const attempt = this.#establish();
|
|
793
803
|
this.#connecting = attempt;
|
|
794
804
|
try {
|
|
@@ -804,15 +814,11 @@ var Browser = class {
|
|
|
804
814
|
}
|
|
805
815
|
async disconnect() {
|
|
806
816
|
if (this.#destroyed) return;
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (this.#connecting !== void 0) {
|
|
813
|
-
await this.#connecting.catch(() => void 0);
|
|
814
|
-
if (this.#destroyed) return;
|
|
815
|
-
}
|
|
817
|
+
const disconnecting = this.#disconnecting;
|
|
818
|
+
const connecting = this.#connecting;
|
|
819
|
+
if (disconnecting !== void 0) await disconnecting;
|
|
820
|
+
if (connecting !== void 0) await connecting.catch(() => void 0);
|
|
821
|
+
if (this.#destroyed) return;
|
|
816
822
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
817
823
|
const attempt = this.#detach();
|
|
818
824
|
this.#disconnecting = attempt;
|
|
@@ -893,6 +899,10 @@ var Browser = class {
|
|
|
893
899
|
return shutdown;
|
|
894
900
|
}
|
|
895
901
|
async #establish() {
|
|
902
|
+
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
903
|
+
await this.#settleExit();
|
|
904
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
905
|
+
if (this.#status === "connected") return;
|
|
896
906
|
this.#assertNotAborted();
|
|
897
907
|
this.#status = "connecting";
|
|
898
908
|
try {
|
|
@@ -966,18 +976,19 @@ var Browser = class {
|
|
|
966
976
|
const connected = this.#status === "connected";
|
|
967
977
|
const client = this.#client;
|
|
968
978
|
const contexts = this.#contexts;
|
|
979
|
+
const process = this.#process;
|
|
969
980
|
this.#unbindTransport();
|
|
970
981
|
this.#unbindProcess();
|
|
971
982
|
this.#process = void 0;
|
|
972
|
-
|
|
983
|
+
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
984
|
+
this.#exitCleanup = cleanup;
|
|
985
|
+
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
973
986
|
this.#client = void 0;
|
|
974
987
|
this.#contexts = [];
|
|
975
988
|
this.#connection = void 0;
|
|
976
989
|
this.#owned = void 0;
|
|
977
990
|
this.#endpoint = void 0;
|
|
978
991
|
if (connected) this.#status = "disconnected";
|
|
979
|
-
this.#destroyContextList(contexts);
|
|
980
|
-
this.#closeClient(client);
|
|
981
992
|
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
982
993
|
if (connected) {
|
|
983
994
|
this.#emitter.emit("disconnect");
|
|
@@ -1137,7 +1148,7 @@ var Browser = class {
|
|
|
1137
1148
|
else {
|
|
1138
1149
|
const found = findSystemBrowser({
|
|
1139
1150
|
...this.#options.browsers,
|
|
1140
|
-
engine: requestedEngine
|
|
1151
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
1141
1152
|
});
|
|
1142
1153
|
executable = found?.executable;
|
|
1143
1154
|
resolvedEngine = found?.engine;
|
|
@@ -1251,6 +1262,7 @@ var Browser = class {
|
|
|
1251
1262
|
this.#registerContext(context);
|
|
1252
1263
|
}
|
|
1253
1264
|
async #destroyResources() {
|
|
1265
|
+
let terminated = false;
|
|
1254
1266
|
try {
|
|
1255
1267
|
await this.#settle();
|
|
1256
1268
|
this.#unbindTransport();
|
|
@@ -1263,17 +1275,22 @@ var Browser = class {
|
|
|
1263
1275
|
this.#contexts = [];
|
|
1264
1276
|
await this.#terminate(this.#process);
|
|
1265
1277
|
this.#process = void 0;
|
|
1266
|
-
|
|
1267
|
-
this.#client = void 0;
|
|
1278
|
+
terminated = true;
|
|
1268
1279
|
} finally {
|
|
1280
|
+
const contexts = this.#contexts;
|
|
1281
|
+
this.#contexts = [];
|
|
1282
|
+
const client = this.#client;
|
|
1283
|
+
this.#client = void 0;
|
|
1269
1284
|
try {
|
|
1270
|
-
await this.#
|
|
1285
|
+
await this.#cleanupLocal(contexts, client);
|
|
1286
|
+
if (terminated) await this.#releaseProfile();
|
|
1271
1287
|
} finally {
|
|
1272
1288
|
this.#finish();
|
|
1273
1289
|
}
|
|
1274
1290
|
}
|
|
1275
1291
|
}
|
|
1276
1292
|
async #closeResources() {
|
|
1293
|
+
let terminated = false;
|
|
1277
1294
|
try {
|
|
1278
1295
|
await this.#settle();
|
|
1279
1296
|
this.#unbindTransport();
|
|
@@ -1284,14 +1301,18 @@ var Browser = class {
|
|
|
1284
1301
|
await this.#closeRemote(client);
|
|
1285
1302
|
const process = this.#process;
|
|
1286
1303
|
if (process !== void 0) {
|
|
1287
|
-
if (!await this.#
|
|
1304
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1288
1305
|
}
|
|
1289
1306
|
this.#process = void 0;
|
|
1290
|
-
|
|
1291
|
-
this.#client = void 0;
|
|
1307
|
+
terminated = true;
|
|
1292
1308
|
} finally {
|
|
1309
|
+
const contexts = this.#contexts;
|
|
1310
|
+
this.#contexts = [];
|
|
1311
|
+
const client = this.#client;
|
|
1312
|
+
this.#client = void 0;
|
|
1293
1313
|
try {
|
|
1294
|
-
await this.#
|
|
1314
|
+
await this.#cleanupLocal(contexts, client);
|
|
1315
|
+
if (terminated) await this.#releaseProfile();
|
|
1295
1316
|
} finally {
|
|
1296
1317
|
this.#finish();
|
|
1297
1318
|
}
|
|
@@ -1300,16 +1321,24 @@ var Browser = class {
|
|
|
1300
1321
|
async #settle() {
|
|
1301
1322
|
await this.#connecting?.catch(() => void 0);
|
|
1302
1323
|
await this.#disconnecting?.catch(() => void 0);
|
|
1324
|
+
await this.#settleExit();
|
|
1325
|
+
}
|
|
1326
|
+
async #settleExit() {
|
|
1327
|
+
const cleanup = this.#exitCleanup;
|
|
1328
|
+
if (cleanup === void 0) return;
|
|
1329
|
+
await cleanup;
|
|
1330
|
+
if (this.#exitCleanup === cleanup) this.#exitCleanup = void 0;
|
|
1303
1331
|
}
|
|
1304
1332
|
async #closeRemote(client) {
|
|
1305
1333
|
let remote = client;
|
|
1306
1334
|
let temporary = false;
|
|
1307
1335
|
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
1336
|
+
const transport = createCDPTransport({
|
|
1337
|
+
url: this.#endpoint,
|
|
1338
|
+
timeout: this.#timeout()
|
|
1339
|
+
});
|
|
1308
1340
|
remote = new _src_core.CDPClient({
|
|
1309
|
-
transport
|
|
1310
|
-
url: this.#endpoint,
|
|
1311
|
-
timeout: this.#timeout()
|
|
1312
|
-
}),
|
|
1341
|
+
transport,
|
|
1313
1342
|
timeout: this.#timeout()
|
|
1314
1343
|
});
|
|
1315
1344
|
try {
|
|
@@ -1357,9 +1386,19 @@ var Browser = class {
|
|
|
1357
1386
|
this.#profile = void 0;
|
|
1358
1387
|
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1359
1388
|
}
|
|
1360
|
-
async #
|
|
1361
|
-
|
|
1362
|
-
|
|
1389
|
+
async #cleanupExitedProcess(process, contexts, client) {
|
|
1390
|
+
let terminated = false;
|
|
1391
|
+
try {
|
|
1392
|
+
await this.#terminate(process);
|
|
1393
|
+
terminated = true;
|
|
1394
|
+
} finally {
|
|
1395
|
+
await this.#cleanupLocal(contexts, client);
|
|
1396
|
+
}
|
|
1397
|
+
if (terminated) await this.#releaseProfile();
|
|
1398
|
+
}
|
|
1399
|
+
async #cleanupLocal(contexts, client) {
|
|
1400
|
+
await this.#destroyContextList(contexts);
|
|
1401
|
+
await this.#closeClient(client);
|
|
1363
1402
|
}
|
|
1364
1403
|
async #waitForProcessWithin(process, timeout) {
|
|
1365
1404
|
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
@@ -1372,20 +1411,55 @@ var Browser = class {
|
|
|
1372
1411
|
throw error;
|
|
1373
1412
|
}
|
|
1374
1413
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1414
|
+
#hasProcessGroup(process) {
|
|
1415
|
+
const pid = process.pid;
|
|
1416
|
+
return globalThis.process.platform !== "win32" && pid !== void 0 && pid > 0;
|
|
1417
|
+
}
|
|
1418
|
+
#inspectProcessGroup(process) {
|
|
1419
|
+
if (!this.#hasProcessGroup(process)) return false;
|
|
1420
|
+
return this.#signalProcess(process, 0);
|
|
1421
|
+
}
|
|
1422
|
+
#signalProcess(process, signal) {
|
|
1423
|
+
const pid = process.pid;
|
|
1377
1424
|
try {
|
|
1378
|
-
process.kill(
|
|
1379
|
-
|
|
1380
|
-
|
|
1425
|
+
if (this.#hasProcessGroup(process) && pid !== void 0) return globalThis.process.kill(-pid, signal);
|
|
1426
|
+
return process.kill(signal);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
|
|
1429
|
+
if (code === "ESRCH") return false;
|
|
1430
|
+
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
1431
|
+
throw new BrowserConnectionError("Failed to signal the browser process", {
|
|
1432
|
+
pid,
|
|
1433
|
+
signal,
|
|
1434
|
+
cause: error
|
|
1435
|
+
});
|
|
1381
1436
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1437
|
+
}
|
|
1438
|
+
async #waitForProcessGroupWithin(process, timeout) {
|
|
1439
|
+
const deadline = performance.now() + timeout;
|
|
1440
|
+
let confirmed = false;
|
|
1441
|
+
while (true) {
|
|
1442
|
+
const alive = this.#inspectProcessGroup(process);
|
|
1443
|
+
if (alive === false) return true;
|
|
1444
|
+
if (alive === true) confirmed = true;
|
|
1445
|
+
const remaining = deadline - performance.now();
|
|
1446
|
+
if (remaining <= 0) return confirmed ? false : void 0;
|
|
1447
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, remaining)));
|
|
1387
1448
|
}
|
|
1388
|
-
|
|
1449
|
+
}
|
|
1450
|
+
async #waitForTerminationWithin(process, timeout, final = false) {
|
|
1451
|
+
const exited = this.#waitForProcessWithin(process, timeout);
|
|
1452
|
+
const drained = this.#waitForProcessGroupWithin(process, timeout);
|
|
1453
|
+
const results = await Promise.all([exited, drained]);
|
|
1454
|
+
return results[0] && (results[1] === true || final && results[1] === void 0);
|
|
1455
|
+
}
|
|
1456
|
+
async #terminate(process) {
|
|
1457
|
+
if (process === void 0 || (process.exitCode !== null || process.signalCode !== null) && this.#inspectProcessGroup(process) === false) return;
|
|
1458
|
+
if (this.#signalProcess(process, "SIGTERM") === false) return;
|
|
1459
|
+
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
1460
|
+
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
1461
|
+
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
1462
|
+
throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: process.pid });
|
|
1389
1463
|
}
|
|
1390
1464
|
#finish() {
|
|
1391
1465
|
this.#unbindTransport();
|
|
@@ -1396,6 +1470,7 @@ var Browser = class {
|
|
|
1396
1470
|
this.#endpoint = void 0;
|
|
1397
1471
|
this.#client = void 0;
|
|
1398
1472
|
this.#process = void 0;
|
|
1473
|
+
this.#exitCleanup = void 0;
|
|
1399
1474
|
this.#contexts = [];
|
|
1400
1475
|
this.#emitter.emit("destroy");
|
|
1401
1476
|
this.#emitter.destroy();
|