@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
package/dist/src/server/index.js
CHANGED
|
@@ -29,7 +29,7 @@ var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser
|
|
|
29
29
|
var BROWSER_HEADLESS_ARG = "--headless=new";
|
|
30
30
|
/** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
|
|
31
31
|
var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
|
|
32
|
-
/**
|
|
32
|
+
/** Bound for each launched-process exit window during TERM-to-KILL teardown. */
|
|
33
33
|
var BROWSER_KILL_GRACE_MS = 3e3;
|
|
34
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. */
|
|
35
35
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
@@ -394,6 +394,10 @@ function findAllInStore(base, platform) {
|
|
|
394
394
|
/**
|
|
395
395
|
* Launch a browser process with raw-CDP debugging flags.
|
|
396
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
|
+
*
|
|
397
401
|
* @param executable - Absolute path to the browser executable
|
|
398
402
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
399
403
|
* @param headless - Whether to launch in headless mode
|
|
@@ -407,7 +411,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
407
411
|
args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
|
|
408
412
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
409
413
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
410
|
-
return spawn(executable, args, {
|
|
414
|
+
return spawn(executable, args, {
|
|
415
|
+
stdio: "ignore",
|
|
416
|
+
detached: process.platform !== "win32"
|
|
417
|
+
});
|
|
411
418
|
}
|
|
412
419
|
/**
|
|
413
420
|
* Poll a browser's CDP version endpoint until it responds or the timeout elapses.
|
|
@@ -498,11 +505,11 @@ var WebSocketCDPTransport = class {
|
|
|
498
505
|
#reject;
|
|
499
506
|
#timer;
|
|
500
507
|
#closeResolve;
|
|
501
|
-
#onSocketClose =
|
|
508
|
+
#onSocketClose = this.#handleSocketClose.bind(this);
|
|
502
509
|
constructor(options) {
|
|
503
510
|
this.#emitter = new Emitter({
|
|
504
|
-
on: options.on,
|
|
505
|
-
error: options.error
|
|
511
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
512
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
506
513
|
});
|
|
507
514
|
this.#url = options.url;
|
|
508
515
|
this.#timeout = options.timeout ?? BROWSER_DEFAULT_TIMEOUT_MS;
|
|
@@ -673,6 +680,9 @@ var WebSocketCDPTransport = class {
|
|
|
673
680
|
});
|
|
674
681
|
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
675
682
|
}
|
|
683
|
+
#handleSocketClose() {
|
|
684
|
+
this.#closeResolve?.();
|
|
685
|
+
}
|
|
676
686
|
};
|
|
677
687
|
//#endregion
|
|
678
688
|
//#region src/server/factories.ts
|
|
@@ -736,15 +746,16 @@ var Browser = class {
|
|
|
736
746
|
#destroyed = false;
|
|
737
747
|
#connecting;
|
|
738
748
|
#disconnecting;
|
|
749
|
+
#exitCleanup;
|
|
739
750
|
#shutdown;
|
|
740
751
|
#transport;
|
|
741
|
-
#onTransportClose =
|
|
742
|
-
#onTransportError =
|
|
743
|
-
#onProcessExit =
|
|
752
|
+
#onTransportClose = this.#handleTransportLoss.bind(this);
|
|
753
|
+
#onTransportError = this.#handleTransportLoss.bind(this);
|
|
754
|
+
#onProcessExit = this.#handleProcessExit.bind(this);
|
|
744
755
|
constructor(options) {
|
|
745
756
|
this.#emitter = new Emitter({
|
|
746
|
-
on: options
|
|
747
|
-
error: options
|
|
757
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
758
|
+
...options?.error !== void 0 ? { error: options.error } : {}
|
|
748
759
|
});
|
|
749
760
|
this.#options = options ?? {};
|
|
750
761
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
@@ -780,14 +791,12 @@ var Browser = class {
|
|
|
780
791
|
}
|
|
781
792
|
async connect() {
|
|
782
793
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
783
|
-
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
784
|
-
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
785
|
-
if (this.#status === "connected") return;
|
|
786
794
|
const active = this.#connecting;
|
|
787
795
|
if (active !== void 0) {
|
|
788
796
|
await active;
|
|
789
797
|
return;
|
|
790
798
|
}
|
|
799
|
+
if (this.#status === "connected") return;
|
|
791
800
|
const attempt = this.#establish();
|
|
792
801
|
this.#connecting = attempt;
|
|
793
802
|
try {
|
|
@@ -803,15 +812,11 @@ var Browser = class {
|
|
|
803
812
|
}
|
|
804
813
|
async disconnect() {
|
|
805
814
|
if (this.#destroyed) return;
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (this.#connecting !== void 0) {
|
|
812
|
-
await this.#connecting.catch(() => void 0);
|
|
813
|
-
if (this.#destroyed) return;
|
|
814
|
-
}
|
|
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;
|
|
815
820
|
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
816
821
|
const attempt = this.#detach();
|
|
817
822
|
this.#disconnecting = attempt;
|
|
@@ -892,6 +897,10 @@ var Browser = class {
|
|
|
892
897
|
return shutdown;
|
|
893
898
|
}
|
|
894
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;
|
|
895
904
|
this.#assertNotAborted();
|
|
896
905
|
this.#status = "connecting";
|
|
897
906
|
try {
|
|
@@ -965,18 +974,19 @@ var Browser = class {
|
|
|
965
974
|
const connected = this.#status === "connected";
|
|
966
975
|
const client = this.#client;
|
|
967
976
|
const contexts = this.#contexts;
|
|
977
|
+
const process = this.#process;
|
|
968
978
|
this.#unbindTransport();
|
|
969
979
|
this.#unbindProcess();
|
|
970
980
|
this.#process = void 0;
|
|
971
|
-
|
|
981
|
+
const cleanup = this.#cleanupExitedProcess(process, contexts, client);
|
|
982
|
+
this.#exitCleanup = cleanup;
|
|
983
|
+
cleanup.catch((error) => this.#emitter.emit("error", error));
|
|
972
984
|
this.#client = void 0;
|
|
973
985
|
this.#contexts = [];
|
|
974
986
|
this.#connection = void 0;
|
|
975
987
|
this.#owned = void 0;
|
|
976
988
|
this.#endpoint = void 0;
|
|
977
989
|
if (connected) this.#status = "disconnected";
|
|
978
|
-
this.#destroyContextList(contexts);
|
|
979
|
-
this.#closeClient(client);
|
|
980
990
|
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
981
991
|
if (connected) {
|
|
982
992
|
this.#emitter.emit("disconnect");
|
|
@@ -1136,7 +1146,7 @@ var Browser = class {
|
|
|
1136
1146
|
else {
|
|
1137
1147
|
const found = findSystemBrowser({
|
|
1138
1148
|
...this.#options.browsers,
|
|
1139
|
-
engine: requestedEngine
|
|
1149
|
+
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
1140
1150
|
});
|
|
1141
1151
|
executable = found?.executable;
|
|
1142
1152
|
resolvedEngine = found?.engine;
|
|
@@ -1250,6 +1260,7 @@ var Browser = class {
|
|
|
1250
1260
|
this.#registerContext(context);
|
|
1251
1261
|
}
|
|
1252
1262
|
async #destroyResources() {
|
|
1263
|
+
let terminated = false;
|
|
1253
1264
|
try {
|
|
1254
1265
|
await this.#settle();
|
|
1255
1266
|
this.#unbindTransport();
|
|
@@ -1262,17 +1273,22 @@ var Browser = class {
|
|
|
1262
1273
|
this.#contexts = [];
|
|
1263
1274
|
await this.#terminate(this.#process);
|
|
1264
1275
|
this.#process = void 0;
|
|
1265
|
-
|
|
1266
|
-
this.#client = void 0;
|
|
1276
|
+
terminated = true;
|
|
1267
1277
|
} finally {
|
|
1278
|
+
const contexts = this.#contexts;
|
|
1279
|
+
this.#contexts = [];
|
|
1280
|
+
const client = this.#client;
|
|
1281
|
+
this.#client = void 0;
|
|
1268
1282
|
try {
|
|
1269
|
-
await this.#
|
|
1283
|
+
await this.#cleanupLocal(contexts, client);
|
|
1284
|
+
if (terminated) await this.#releaseProfile();
|
|
1270
1285
|
} finally {
|
|
1271
1286
|
this.#finish();
|
|
1272
1287
|
}
|
|
1273
1288
|
}
|
|
1274
1289
|
}
|
|
1275
1290
|
async #closeResources() {
|
|
1291
|
+
let terminated = false;
|
|
1276
1292
|
try {
|
|
1277
1293
|
await this.#settle();
|
|
1278
1294
|
this.#unbindTransport();
|
|
@@ -1283,14 +1299,18 @@ var Browser = class {
|
|
|
1283
1299
|
await this.#closeRemote(client);
|
|
1284
1300
|
const process = this.#process;
|
|
1285
1301
|
if (process !== void 0) {
|
|
1286
|
-
if (!await this.#
|
|
1302
|
+
if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
|
|
1287
1303
|
}
|
|
1288
1304
|
this.#process = void 0;
|
|
1289
|
-
|
|
1290
|
-
this.#client = void 0;
|
|
1305
|
+
terminated = true;
|
|
1291
1306
|
} finally {
|
|
1307
|
+
const contexts = this.#contexts;
|
|
1308
|
+
this.#contexts = [];
|
|
1309
|
+
const client = this.#client;
|
|
1310
|
+
this.#client = void 0;
|
|
1292
1311
|
try {
|
|
1293
|
-
await this.#
|
|
1312
|
+
await this.#cleanupLocal(contexts, client);
|
|
1313
|
+
if (terminated) await this.#releaseProfile();
|
|
1294
1314
|
} finally {
|
|
1295
1315
|
this.#finish();
|
|
1296
1316
|
}
|
|
@@ -1299,6 +1319,13 @@ var Browser = class {
|
|
|
1299
1319
|
async #settle() {
|
|
1300
1320
|
await this.#connecting?.catch(() => void 0);
|
|
1301
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;
|
|
1302
1329
|
}
|
|
1303
1330
|
async #closeRemote(client) {
|
|
1304
1331
|
let remote = client;
|
|
@@ -1356,9 +1383,19 @@ var Browser = class {
|
|
|
1356
1383
|
this.#profile = void 0;
|
|
1357
1384
|
if (profile !== void 0) await removeBrowserProfile(profile);
|
|
1358
1385
|
}
|
|
1359
|
-
async #
|
|
1360
|
-
|
|
1361
|
-
|
|
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);
|
|
1362
1399
|
}
|
|
1363
1400
|
async #waitForProcessWithin(process, timeout) {
|
|
1364
1401
|
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
@@ -1371,20 +1408,55 @@ var Browser = class {
|
|
|
1371
1408
|
throw error;
|
|
1372
1409
|
}
|
|
1373
1410
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
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;
|
|
1376
1421
|
try {
|
|
1377
|
-
process.kill(
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
+
});
|
|
1380
1433
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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)));
|
|
1386
1445
|
}
|
|
1387
|
-
|
|
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 });
|
|
1388
1460
|
}
|
|
1389
1461
|
#finish() {
|
|
1390
1462
|
this.#unbindTransport();
|
|
@@ -1395,6 +1467,7 @@ var Browser = class {
|
|
|
1395
1467
|
this.#endpoint = void 0;
|
|
1396
1468
|
this.#client = void 0;
|
|
1397
1469
|
this.#process = void 0;
|
|
1470
|
+
this.#exitCleanup = void 0;
|
|
1398
1471
|
this.#contexts = [];
|
|
1399
1472
|
this.#emitter.emit("destroy");
|
|
1400
1473
|
this.#emitter.destroy();
|