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