@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.
@@ -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
- /** Grace period after SIGTERM before a launched process is escalated to SIGKILL during teardown. */
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, { stdio: "ignore" });
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 = () => this.#closeResolve?.();
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 = () => this.#handleTransportLoss();
743
- #onTransportError = () => this.#handleTransportLoss();
744
- #onProcessExit = () => this.#handleProcessExit();
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?.on,
748
- error: options?.error
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 active = this.#disconnecting;
808
- if (active !== void 0) {
809
- await active;
810
- return;
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
- this.#releaseProfile().catch((error) => this.#emitter.emit("error", error));
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
- await this.#closeClient(client);
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.#releaseProfile();
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.#waitForProcessWithin(process, 3e3)) await this.#terminate(process);
1303
+ if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
1288
1304
  }
1289
1305
  this.#process = void 0;
1290
- await this.#closeClient(client);
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.#releaseProfile();
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 #waitForProcess(process) {
1361
- if (process.exitCode !== null || process.signalCode !== null) return;
1362
- await (0, node_events.once)(process, "exit");
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
- async #terminate(process) {
1376
- if (process === void 0 || process.exitCode !== null || process.signalCode !== null) return;
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("SIGTERM");
1379
- } catch {
1380
- return;
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
- if (await this.#waitForProcessWithin(process, 3e3)) return;
1383
- try {
1384
- process.kill("SIGKILL");
1385
- } catch {
1386
- return;
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
- await this.#waitForProcess(process);
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();