@orkestrel/browser 0.0.6 → 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/dist/src/core/index.cjs +308 -262
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +308 -262
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +120 -47
- 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 +120 -47
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -16
|
@@ -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;
|
|
@@ -395,6 +395,10 @@ function findAllInStore(base, platform) {
|
|
|
395
395
|
/**
|
|
396
396
|
* Launch a browser process with raw-CDP debugging flags.
|
|
397
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
|
+
*
|
|
398
402
|
* @param executable - Absolute path to the browser executable
|
|
399
403
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
400
404
|
* @param headless - Whether to launch in headless mode
|
|
@@ -408,7 +412,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
408
412
|
args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
|
|
409
413
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
410
414
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
411
|
-
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
|
+
});
|
|
412
419
|
}
|
|
413
420
|
/**
|
|
414
421
|
* Poll a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
@@ -499,11 +506,11 @@ var WebSocketCDPTransport = class {
|
|
|
499
506
|
#reject;
|
|
500
507
|
#timer;
|
|
501
508
|
#closeResolve;
|
|
502
|
-
#onSocketClose =
|
|
509
|
+
#onSocketClose = this.#handleSocketClose.bind(this);
|
|
503
510
|
constructor(options) {
|
|
504
511
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
505
|
-
on: options.on,
|
|
506
|
-
error: options.error
|
|
512
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
513
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
507
514
|
});
|
|
508
515
|
this.#url = options.url;
|
|
509
516
|
this.#timeout = options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -674,6 +681,9 @@ var WebSocketCDPTransport = class {
|
|
|
674
681
|
});
|
|
675
682
|
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
676
683
|
}
|
|
684
|
+
#handleSocketClose() {
|
|
685
|
+
this.#closeResolve?.();
|
|
686
|
+
}
|
|
677
687
|
};
|
|
678
688
|
//#endregion
|
|
679
689
|
//#region src/server/factories.ts
|
|
@@ -737,15 +747,16 @@ var Browser = class {
|
|
|
737
747
|
#destroyed = false;
|
|
738
748
|
#connecting;
|
|
739
749
|
#disconnecting;
|
|
750
|
+
#exitCleanup;
|
|
740
751
|
#shutdown;
|
|
741
752
|
#transport;
|
|
742
|
-
#onTransportClose =
|
|
743
|
-
#onTransportError =
|
|
744
|
-
#onProcessExit =
|
|
753
|
+
#onTransportClose = this.#handleTransportLoss.bind(this);
|
|
754
|
+
#onTransportError = this.#handleTransportLoss.bind(this);
|
|
755
|
+
#onProcessExit = this.#handleProcessExit.bind(this);
|
|
745
756
|
constructor(options) {
|
|
746
757
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
747
|
-
on: options
|
|
748
|
-
error: options
|
|
758
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
759
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
749
760
|
});
|
|
750
761
|
this.#options = options ?? {};
|
|
751
762
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
@@ -781,14 +792,12 @@ var Browser = class {
|
|
|
781
792
|
}
|
|
782
793
|
async connect() {
|
|
783
794
|
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
795
|
const active = this.#connecting;
|
|
788
796
|
if (active !== void 0) {
|
|
789
797
|
await active;
|
|
790
798
|
return;
|
|
791
799
|
}
|
|
800
|
+
if (this.#status === "connected") return;
|
|
792
801
|
const attempt = this.#establish();
|
|
793
802
|
this.#connecting = attempt;
|
|
794
803
|
try {
|
|
@@ -804,15 +813,11 @@ var Browser = class {
|
|
|
804
813
|
}
|
|
805
814
|
async disconnect() {
|
|
806
815
|
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
|
-
}
|
|
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;
|
|
816
821
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
817
822
|
const attempt = this.#detach();
|
|
818
823
|
this.#disconnecting = attempt;
|
|
@@ -893,6 +898,10 @@ var Browser = class {
|
|
|
893
898
|
return shutdown;
|
|
894
899
|
}
|
|
895
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;
|
|
896
905
|
this.#assertNotAborted();
|
|
897
906
|
this.#status = "connecting";
|
|
898
907
|
try {
|
|
@@ -966,18 +975,19 @@ var Browser = class {
|
|
|
966
975
|
const connected = this.#status === "connected";
|
|
967
976
|
const client = this.#client;
|
|
968
977
|
const contexts = this.#contexts;
|
|
978
|
+
const process = this.#process;
|
|
969
979
|
this.#unbindTransport();
|
|
970
980
|
this.#unbindProcess();
|
|
971
981
|
this.#process = void 0;
|
|
972
|
-
|
|
982
|
+
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
983
|
+
this.#exitCleanup = cleanup;
|
|
984
|
+
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
973
985
|
this.#client = void 0;
|
|
974
986
|
this.#contexts = [];
|
|
975
987
|
this.#connection = void 0;
|
|
976
988
|
this.#owned = void 0;
|
|
977
989
|
this.#endpoint = void 0;
|
|
978
990
|
if (connected) this.#status = "disconnected";
|
|
979
|
-
this.#destroyContextList(contexts);
|
|
980
|
-
this.#closeClient(client);
|
|
981
991
|
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
982
992
|
if (connected) {
|
|
983
993
|
this.#emitter.emit("disconnect");
|
|
@@ -1137,7 +1147,7 @@ var Browser = class {
|
|
|
1137
1147
|
else {
|
|
1138
1148
|
const found = findSystemBrowser({
|
|
1139
1149
|
...this.#options.browsers,
|
|
1140
|
-
engine: requestedEngine
|
|
1150
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
1141
1151
|
});
|
|
1142
1152
|
executable = found?.executable;
|
|
1143
1153
|
resolvedEngine = found?.engine;
|
|
@@ -1251,6 +1261,7 @@ var Browser = class {
|
|
|
1251
1261
|
this.#registerContext(context);
|
|
1252
1262
|
}
|
|
1253
1263
|
async #destroyResources() {
|
|
1264
|
+
let terminated = false;
|
|
1254
1265
|
try {
|
|
1255
1266
|
await this.#settle();
|
|
1256
1267
|
this.#unbindTransport();
|
|
@@ -1263,17 +1274,22 @@ var Browser = class {
|
|
|
1263
1274
|
this.#contexts = [];
|
|
1264
1275
|
await this.#terminate(this.#process);
|
|
1265
1276
|
this.#process = void 0;
|
|
1266
|
-
|
|
1267
|
-
this.#client = void 0;
|
|
1277
|
+
terminated = true;
|
|
1268
1278
|
} finally {
|
|
1279
|
+
const contexts = this.#contexts;
|
|
1280
|
+
this.#contexts = [];
|
|
1281
|
+
const client = this.#client;
|
|
1282
|
+
this.#client = void 0;
|
|
1269
1283
|
try {
|
|
1270
|
-
await this.#
|
|
1284
|
+
await this.#cleanupLocal(contexts, client);
|
|
1285
|
+
if (terminated) await this.#releaseProfile();
|
|
1271
1286
|
} finally {
|
|
1272
1287
|
this.#finish();
|
|
1273
1288
|
}
|
|
1274
1289
|
}
|
|
1275
1290
|
}
|
|
1276
1291
|
async #closeResources() {
|
|
1292
|
+
let terminated = false;
|
|
1277
1293
|
try {
|
|
1278
1294
|
await this.#settle();
|
|
1279
1295
|
this.#unbindTransport();
|
|
@@ -1284,14 +1300,18 @@ var Browser = class {
|
|
|
1284
1300
|
await this.#closeRemote(client);
|
|
1285
1301
|
const process = this.#process;
|
|
1286
1302
|
if (process !== void 0) {
|
|
1287
|
-
if (!await this.#
|
|
1303
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1288
1304
|
}
|
|
1289
1305
|
this.#process = void 0;
|
|
1290
|
-
|
|
1291
|
-
this.#client = void 0;
|
|
1306
|
+
terminated = true;
|
|
1292
1307
|
} finally {
|
|
1308
|
+
const contexts = this.#contexts;
|
|
1309
|
+
this.#contexts = [];
|
|
1310
|
+
const client = this.#client;
|
|
1311
|
+
this.#client = void 0;
|
|
1293
1312
|
try {
|
|
1294
|
-
await this.#
|
|
1313
|
+
await this.#cleanupLocal(contexts, client);
|
|
1314
|
+
if (terminated) await this.#releaseProfile();
|
|
1295
1315
|
} finally {
|
|
1296
1316
|
this.#finish();
|
|
1297
1317
|
}
|
|
@@ -1300,6 +1320,13 @@ var Browser = class {
|
|
|
1300
1320
|
async #settle() {
|
|
1301
1321
|
await this.#connecting?.catch(() => void 0);
|
|
1302
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;
|
|
1303
1330
|
}
|
|
1304
1331
|
async #closeRemote(client) {
|
|
1305
1332
|
let remote = client;
|
|
@@ -1357,9 +1384,19 @@ var Browser = class {
|
|
|
1357
1384
|
this.#profile = void 0;
|
|
1358
1385
|
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1359
1386
|
}
|
|
1360
|
-
async #
|
|
1361
|
-
|
|
1362
|
-
|
|
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);
|
|
1363
1400
|
}
|
|
1364
1401
|
async #waitForProcessWithin(process, timeout) {
|
|
1365
1402
|
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
@@ -1372,20 +1409,55 @@ var Browser = class {
|
|
|
1372
1409
|
throw error;
|
|
1373
1410
|
}
|
|
1374
1411
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
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;
|
|
1377
1422
|
try {
|
|
1378
|
-
process.kill(
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
+
});
|
|
1381
1434
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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)));
|
|
1387
1446
|
}
|
|
1388
|
-
|
|
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 });
|
|
1389
1461
|
}
|
|
1390
1462
|
#finish() {
|
|
1391
1463
|
this.#unbindTransport();
|
|
@@ -1396,6 +1468,7 @@ var Browser = class {
|
|
|
1396
1468
|
this.#endpoint = void 0;
|
|
1397
1469
|
this.#client = void 0;
|
|
1398
1470
|
this.#process = void 0;
|
|
1471
|
+
this.#exitCleanup = void 0;
|
|
1399
1472
|
this.#contexts = [];
|
|
1400
1473
|
this.#emitter.emit("destroy");
|
|
1401
1474
|
this.#emitter.destroy();
|