@orkestrel/browser 0.0.5 → 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.
@@ -1,17 +1,18 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _src_core = require("../core/index.cjs");
3
+ let _orkestrel_contract = require("@orkestrel/contract");
3
4
  let node_fs = require("node:fs");
5
+ let node_fs_promises = require("node:fs/promises");
6
+ let node_os = require("node:os");
4
7
  let node_path = require("node:path");
5
8
  let node_child_process = require("node:child_process");
6
9
  let node_timers_promises = require("node:timers/promises");
7
- let _orkestrel_contract = require("@orkestrel/contract");
8
10
  let node_crypto = require("node:crypto");
9
11
  let node_http = require("node:http");
10
12
  let node_https = require("node:https");
11
13
  let _orkestrel_emitter = require("@orkestrel/emitter");
12
14
  let _orkestrel_websocket = require("@orkestrel/websocket");
13
15
  let node_events = require("node:events");
14
- let node_fs_promises = require("node:fs/promises");
15
16
  //#region src/server/constants.ts
16
17
  /** Default CDP port probed for an existing browser and used for launches. */
17
18
  var BROWSER_DEFAULT_CDP_PORT = 9222;
@@ -27,7 +28,9 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
27
28
  var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
28
29
  /** Flag enabling headless mode on a launched browser process. */
29
30
  var BROWSER_HEADLESS_ARG = "--headless=new";
30
- /** Grace period after SIGTERM before a launched process is escalated to SIGKILL during teardown. */
31
+ /** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
32
+ var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
33
+ /** Bound for each launched-process exit window during TERM-to-KILL teardown. */
31
34
  var BROWSER_KILL_GRACE_MS = 3e3;
32
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. */
33
36
  var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
@@ -158,7 +161,7 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
158
161
  * @returns True when value is a BrowserConnectionError instance
159
162
  */
160
163
  function isBrowserConnectionError(value) {
161
- return value instanceof BrowserConnectionError;
164
+ return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
162
165
  }
163
166
  /**
164
167
  * Narrow an unknown value to BrowserNotConnectedError.
@@ -167,7 +170,7 @@ function isBrowserConnectionError(value) {
167
170
  * @returns True when value is a BrowserNotConnectedError instance
168
171
  */
169
172
  function isBrowserNotConnectedError(value) {
170
- return value instanceof BrowserNotConnectedError;
173
+ return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
171
174
  }
172
175
  /**
173
176
  * Narrow an unknown value to BrowserDestroyedError.
@@ -176,7 +179,7 @@ function isBrowserNotConnectedError(value) {
176
179
  * @returns True when value is a BrowserDestroyedError instance
177
180
  */
178
181
  function isBrowserDestroyedError(value) {
179
- return value instanceof BrowserDestroyedError;
182
+ return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
180
183
  }
181
184
  //#endregion
182
185
  //#region src/server/helpers.ts
@@ -264,6 +267,44 @@ function browserToEngine(browser) {
264
267
  if (browser?.includes("Chrome/") === true) return "chrome";
265
268
  return "chromium";
266
269
  }
270
+ /**
271
+ * Resolve a persistent caller profile or create an isolated temporary one.
272
+ *
273
+ * @param path - Optional caller-owned persistent user-data directory
274
+ * @returns The resolved profile and whether the library owns its lifecycle
275
+ */
276
+ async function createBrowserProfile(path) {
277
+ if (path !== void 0) return {
278
+ path,
279
+ temporary: false
280
+ };
281
+ return {
282
+ path: await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), BROWSER_PROFILE_PREFIX)),
283
+ temporary: true
284
+ };
285
+ }
286
+ /**
287
+ * Remove a library-owned isolated browser profile.
288
+ *
289
+ * @remarks
290
+ * The path must remain a direct child of the operating-system temp directory
291
+ * with {@link BROWSER_PROFILE_PREFIX}; this defensive check prevents a
292
+ * malformed profile value from widening recursive deletion.
293
+ *
294
+ * @param profile - Resolved profile lifecycle result
295
+ */
296
+ async function removeBrowserProfile(profile) {
297
+ if (!profile.temporary) return;
298
+ const path = (0, node_path.resolve)(profile.path);
299
+ const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
300
+ if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new Error("Refusing to remove an unsafe browser profile path");
301
+ await (0, node_fs_promises.rm)(path, {
302
+ recursive: true,
303
+ force: true,
304
+ maxRetries: 3,
305
+ retryDelay: 100
306
+ });
307
+ }
267
308
  /** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
268
309
  function findEnvOverride(env) {
269
310
  return findAllEnvOverrides(env)[0];
@@ -354,6 +395,10 @@ function findAllInStore(base, platform) {
354
395
  /**
355
396
  * Launch a browser process with raw-CDP debugging flags.
356
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
+ *
357
402
  * @param executable - Absolute path to the browser executable
358
403
  * @param port - Port the browser exposes its CDP endpoint on
359
404
  * @param headless - Whether to launch in headless mode
@@ -367,7 +412,10 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
367
412
  args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
368
413
  if (headless) args.push(BROWSER_HEADLESS_ARG);
369
414
  if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
370
- 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
+ });
371
419
  }
372
420
  /**
373
421
  * Poll a browser's CDP version endpoint until it responds or the timeout elapses.
@@ -416,7 +464,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
416
464
  const response = await fetch(url, { signal: controller.signal });
417
465
  if (!response.ok) return [];
418
466
  const list = await response.json();
419
- if (!Array.isArray(list)) return [];
467
+ if (!(0, _orkestrel_contract.isArray)(list)) return [];
420
468
  const targets = [];
421
469
  for (const entry of list) {
422
470
  if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
@@ -458,11 +506,11 @@ var WebSocketCDPTransport = class {
458
506
  #reject;
459
507
  #timer;
460
508
  #closeResolve;
461
- #onSocketClose = () => this.#closeResolve?.();
509
+ #onSocketClose = this.#handleSocketClose.bind(this);
462
510
  constructor(options) {
463
511
  this.#emitter = new _orkestrel_emitter.Emitter({
464
- on: options.on,
465
- error: options.error
512
+ ...options.on !== void 0 ? { on: options.on } : {},
513
+ ...options.error !== void 0 ? { error: options.error } : {}
466
514
  });
467
515
  this.#url = options.url;
468
516
  this.#timeout = options.timeout ?? _src_core.BROWSER_DEFAULT_TIMEOUT_MS;
@@ -480,12 +528,12 @@ var WebSocketCDPTransport = class {
480
528
  }
481
529
  this.#socket?.destroy();
482
530
  this.#socket = void 0;
483
- const attempt = this.#open();
484
- this.#starting = attempt;
531
+ const transition = this.#open();
532
+ this.#starting = transition;
485
533
  try {
486
- await attempt;
534
+ await transition;
487
535
  } finally {
488
- if (this.#starting === attempt) this.#starting = void 0;
536
+ if (this.#starting === transition) this.#starting = void 0;
489
537
  }
490
538
  }
491
539
  async send(data) {
@@ -499,16 +547,21 @@ var WebSocketCDPTransport = class {
499
547
  await active;
500
548
  return;
501
549
  }
502
- const attempt = this.#stop();
503
- this.#closing = attempt;
550
+ const transition = this.#stop();
551
+ this.#closing = transition;
504
552
  try {
505
- await attempt;
553
+ await transition;
506
554
  } finally {
507
- if (this.#closing === attempt) this.#closing = void 0;
555
+ if (this.#closing === transition) this.#closing = void 0;
508
556
  }
509
557
  }
510
558
  async #open() {
511
- const url = new URL(this.#url);
559
+ const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
560
+ if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
561
+ url: this.#url,
562
+ error: parsed.error
563
+ });
564
+ const url = parsed.value;
512
565
  if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
513
566
  const key = (0, node_crypto.randomBytes)(16).toString("base64");
514
567
  const deferred = Promise.withResolvers();
@@ -628,6 +681,9 @@ var WebSocketCDPTransport = class {
628
681
  });
629
682
  socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
630
683
  }
684
+ #handleSocketClose() {
685
+ this.#closeResolve?.();
686
+ }
631
687
  };
632
688
  //#endregion
633
689
  //#region src/server/factories.ts
@@ -670,6 +726,9 @@ function createScreenshotWriter() {
670
726
  }
671
727
  //#endregion
672
728
  //#region src/server/Browser.ts
729
+ /**
730
+ * Discovers, launches, connects to, and owns Chromium-family browser sessions.
731
+ */
673
732
  var Browser = class {
674
733
  #emitter;
675
734
  #options;
@@ -683,19 +742,21 @@ var Browser = class {
683
742
  #endpoint;
684
743
  #client;
685
744
  #process;
745
+ #profile;
686
746
  #contexts = [];
687
747
  #destroyed = false;
688
748
  #connecting;
689
749
  #disconnecting;
750
+ #exitCleanup;
690
751
  #shutdown;
691
752
  #transport;
692
- #onTransportClose = () => this.#handleTransportLoss();
693
- #onTransportError = () => this.#handleTransportLoss();
694
- #onProcessExit = () => this.#handleProcessExit();
753
+ #onTransportClose = this.#handleTransportLoss.bind(this);
754
+ #onTransportError = this.#handleTransportLoss.bind(this);
755
+ #onProcessExit = this.#handleProcessExit.bind(this);
695
756
  constructor(options) {
696
757
  this.#emitter = new _orkestrel_emitter.Emitter({
697
- on: options?.on,
698
- error: options?.error
758
+ ...options?.on !== void 0 ? { on: options.on } : {},
759
+ ...options?.error !== void 0 ? { error: options.error } : {}
699
760
  });
700
761
  this.#options = options ?? {};
701
762
  this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
@@ -731,14 +792,12 @@ var Browser = class {
731
792
  }
732
793
  async connect() {
733
794
  if (this.#destroyed) throw new BrowserDestroyedError();
734
- if (this.#disconnecting !== void 0) await this.#disconnecting;
735
- if (this.#destroyed) throw new BrowserDestroyedError();
736
- if (this.#status === "connected") return;
737
795
  const active = this.#connecting;
738
796
  if (active !== void 0) {
739
797
  await active;
740
798
  return;
741
799
  }
800
+ if (this.#status === "connected") return;
742
801
  const attempt = this.#establish();
743
802
  this.#connecting = attempt;
744
803
  try {
@@ -754,15 +813,11 @@ var Browser = class {
754
813
  }
755
814
  async disconnect() {
756
815
  if (this.#destroyed) return;
757
- const active = this.#disconnecting;
758
- if (active !== void 0) {
759
- await active;
760
- return;
761
- }
762
- if (this.#connecting !== void 0) {
763
- await this.#connecting.catch(() => void 0);
764
- if (this.#destroyed) return;
765
- }
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;
766
821
  if (this.#status !== "connected" || this.#client === void 0) return;
767
822
  const attempt = this.#detach();
768
823
  this.#disconnecting = attempt;
@@ -780,6 +835,35 @@ var Browser = class {
780
835
  contexts() {
781
836
  return [...this.#contexts];
782
837
  }
838
+ async isolate(options) {
839
+ if (this.#destroyed) throw new BrowserDestroyedError();
840
+ const client = this.#client;
841
+ if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
842
+ (0, _src_core.validateBrowserContextOptions)(options);
843
+ const params = { disposeOnDetach: false };
844
+ if (options?.proxy !== void 0) {
845
+ params["proxyServer"] = options.proxy.server;
846
+ if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
847
+ }
848
+ if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
849
+ const result = await client.send("Target.createBrowserContext", params);
850
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
851
+ const id = result["browserContextId"];
852
+ const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
853
+ try {
854
+ if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
855
+ behavior: options.downloads.named === true ? "allowAndName" : "allow",
856
+ browserContextId: id,
857
+ downloadPath: options.downloads.path,
858
+ eventsEnabled: true
859
+ });
860
+ } catch (error) {
861
+ await context.close().catch(() => void 0);
862
+ throw error;
863
+ }
864
+ this.#registerContext(context);
865
+ return context;
866
+ }
783
867
  async create(options) {
784
868
  if (this.#destroyed) throw new BrowserDestroyedError();
785
869
  const client = this.#client;
@@ -787,7 +871,7 @@ var Browser = class {
787
871
  let context = this.#contexts[0];
788
872
  if (context === void 0) {
789
873
  context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
790
- this.#contexts.push(context);
874
+ this.#registerContext(context);
791
875
  }
792
876
  const page = await context.create(options);
793
877
  this.#emitter.emit("page", page);
@@ -814,6 +898,10 @@ var Browser = class {
814
898
  return shutdown;
815
899
  }
816
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;
817
905
  this.#assertNotAborted();
818
906
  this.#status = "connecting";
819
907
  try {
@@ -840,8 +928,8 @@ var Browser = class {
840
928
  if (this.#destroyed) throw new BrowserDestroyedError();
841
929
  this.#status = "error";
842
930
  this.#emitter.emit("error", error);
843
- if (error instanceof BrowserConnectionError) throw error;
844
- throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), { executable: this.#options.executable });
931
+ if (isBrowserConnectionError(error)) throw error;
932
+ throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), { executable: this.#options.executable });
845
933
  }
846
934
  }
847
935
  async #detach() {
@@ -887,17 +975,19 @@ var Browser = class {
887
975
  const connected = this.#status === "connected";
888
976
  const client = this.#client;
889
977
  const contexts = this.#contexts;
978
+ const process = this.#process;
890
979
  this.#unbindTransport();
891
980
  this.#unbindProcess();
892
981
  this.#process = void 0;
982
+ const cleanup = this.#cleanupExitedProcess(process, contexts, client);
983
+ this.#exitCleanup = cleanup;
984
+ cleanup.catch((error) => this.#emitter.emit("error", error));
893
985
  this.#client = void 0;
894
986
  this.#contexts = [];
895
987
  this.#connection = void 0;
896
988
  this.#owned = void 0;
897
989
  this.#endpoint = void 0;
898
990
  if (connected) this.#status = "disconnected";
899
- this.#destroyContextList(contexts);
900
- this.#closeClient(client);
901
991
  this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
902
992
  if (connected) {
903
993
  this.#emitter.emit("disconnect");
@@ -1057,15 +1147,23 @@ var Browser = class {
1057
1147
  else {
1058
1148
  const found = findSystemBrowser({
1059
1149
  ...this.#options.browsers,
1060
- engine: requestedEngine
1150
+ ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
1061
1151
  });
1062
1152
  executable = found?.executable;
1063
1153
  resolvedEngine = found?.engine;
1064
1154
  }
1065
1155
  if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
1066
1156
  this.#engine = resolvedEngine ?? "chromium";
1067
- const profile = this.#options.profile;
1068
- const process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile, this.#options.args);
1157
+ await this.#releaseProfile();
1158
+ const profile = await createBrowserProfile(this.#options.profile);
1159
+ this.#profile = profile;
1160
+ let process;
1161
+ try {
1162
+ process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
1163
+ } catch (error) {
1164
+ await this.#releaseProfile();
1165
+ throw error;
1166
+ }
1069
1167
  this.#process = process;
1070
1168
  let client;
1071
1169
  try {
@@ -1079,7 +1177,7 @@ var Browser = class {
1079
1177
  timeout: this.#timeout()
1080
1178
  });
1081
1179
  await this.#raceAbort(client.connect());
1082
- const connection = profile === void 0 ? "launch" : "persistent";
1180
+ const connection = this.#options.profile === void 0 ? "launch" : "persistent";
1083
1181
  this.#client = client;
1084
1182
  this.#endpoint = endpoint;
1085
1183
  this.#connection = connection;
@@ -1095,6 +1193,7 @@ var Browser = class {
1095
1193
  this.#unbindProcess();
1096
1194
  await this.#closeClient(client);
1097
1195
  await this.#terminate(process);
1196
+ await this.#releaseProfile();
1098
1197
  this.#process = void 0;
1099
1198
  this.#client = void 0;
1100
1199
  this.#endpoint = void 0;
@@ -1111,16 +1210,16 @@ var Browser = class {
1111
1210
  const signal = AbortSignal.any([controller.signal, this.#signal()]);
1112
1211
  const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
1113
1212
  const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
1114
- const code = typeof values[0] === "number" ? values[0] : null;
1115
- const exitSignal = typeof values[1] === "string" ? values[1] : null;
1213
+ const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
1214
+ const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
1116
1215
  throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
1117
1216
  });
1118
1217
  try {
1119
1218
  return await Promise.race([ready, exited]);
1120
1219
  } catch (error) {
1121
- if (error instanceof BrowserConnectionError) throw error;
1220
+ if (isBrowserConnectionError(error)) throw error;
1122
1221
  if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
1123
- throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), context);
1222
+ throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), context);
1124
1223
  } finally {
1125
1224
  controller.abort();
1126
1225
  ready.catch(() => void 0);
@@ -1145,7 +1244,7 @@ var Browser = class {
1145
1244
  } catch {
1146
1245
  return;
1147
1246
  }
1148
- if (!(0, _orkestrel_contract.isRecord)(result) || !Array.isArray(result["targetInfos"])) return;
1247
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
1149
1248
  const pages = [];
1150
1249
  for (const target of result["targetInfos"]) {
1151
1250
  if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || target["type"] !== "page" || !(0, _orkestrel_contract.isString)(target["title"]) || !(0, _orkestrel_contract.isString)(target["url"])) continue;
@@ -1159,9 +1258,10 @@ var Browser = class {
1159
1258
  if (pages.length === 0 || this.#contexts.length > 0) return;
1160
1259
  const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
1161
1260
  await context.sync(pages);
1162
- this.#contexts.push(context);
1261
+ this.#registerContext(context);
1163
1262
  }
1164
1263
  async #destroyResources() {
1264
+ let terminated = false;
1165
1265
  try {
1166
1266
  await this.#settle();
1167
1267
  this.#unbindTransport();
@@ -1174,13 +1274,22 @@ var Browser = class {
1174
1274
  this.#contexts = [];
1175
1275
  await this.#terminate(this.#process);
1176
1276
  this.#process = void 0;
1177
- await this.#closeClient(client);
1178
- this.#client = void 0;
1277
+ terminated = true;
1179
1278
  } finally {
1180
- this.#finish();
1279
+ const contexts = this.#contexts;
1280
+ this.#contexts = [];
1281
+ const client = this.#client;
1282
+ this.#client = void 0;
1283
+ try {
1284
+ await this.#cleanupLocal(contexts, client);
1285
+ if (terminated) await this.#releaseProfile();
1286
+ } finally {
1287
+ this.#finish();
1288
+ }
1181
1289
  }
1182
1290
  }
1183
1291
  async #closeResources() {
1292
+ let terminated = false;
1184
1293
  try {
1185
1294
  await this.#settle();
1186
1295
  this.#unbindTransport();
@@ -1191,18 +1300,33 @@ var Browser = class {
1191
1300
  await this.#closeRemote(client);
1192
1301
  const process = this.#process;
1193
1302
  if (process !== void 0) {
1194
- if (!await this.#waitForProcessWithin(process, 3e3)) await this.#terminate(process);
1303
+ if (!await this.#waitForTerminationWithin(process, 3e3)) await this.#terminate(process);
1195
1304
  }
1196
1305
  this.#process = void 0;
1197
- await this.#closeClient(client);
1198
- this.#client = void 0;
1306
+ terminated = true;
1199
1307
  } finally {
1200
- this.#finish();
1308
+ const contexts = this.#contexts;
1309
+ this.#contexts = [];
1310
+ const client = this.#client;
1311
+ this.#client = void 0;
1312
+ try {
1313
+ await this.#cleanupLocal(contexts, client);
1314
+ if (terminated) await this.#releaseProfile();
1315
+ } finally {
1316
+ this.#finish();
1317
+ }
1201
1318
  }
1202
1319
  }
1203
1320
  async #settle() {
1204
1321
  await this.#connecting?.catch(() => void 0);
1205
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;
1206
1330
  }
1207
1331
  async #closeRemote(client) {
1208
1332
  let remote = client;
@@ -1229,12 +1353,20 @@ var Browser = class {
1229
1353
  if (temporary) await this.#closeClient(remote);
1230
1354
  }
1231
1355
  async #closeContexts() {
1232
- for (const context of this.#contexts) try {
1356
+ for (const context of [...this.#contexts]) try {
1233
1357
  await context.close();
1234
1358
  } catch {}
1235
1359
  }
1236
1360
  async #destroyContexts() {
1237
- await this.#destroyContextList(this.#contexts);
1361
+ await this.#destroyContextList([...this.#contexts]);
1362
+ }
1363
+ #registerContext(context) {
1364
+ this.#contexts.push(context);
1365
+ context.emitter.on("close", () => {
1366
+ const index = this.#contexts.indexOf(context);
1367
+ if (index >= 0) this.#contexts.splice(index, 1);
1368
+ });
1369
+ this.#emitter.emit("context", context);
1238
1370
  }
1239
1371
  async #destroyContextList(contexts) {
1240
1372
  for (const context of contexts) try {
@@ -1247,9 +1379,24 @@ var Browser = class {
1247
1379
  await client.close();
1248
1380
  } catch {}
1249
1381
  }
1250
- async #waitForProcess(process) {
1251
- if (process.exitCode !== null || process.signalCode !== null) return;
1252
- await (0, node_events.once)(process, "exit");
1382
+ async #releaseProfile() {
1383
+ const profile = this.#profile;
1384
+ this.#profile = void 0;
1385
+ if (profile !== void 0) await removeBrowserProfile(profile);
1386
+ }
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);
1253
1400
  }
1254
1401
  async #waitForProcessWithin(process, timeout) {
1255
1402
  if (process.exitCode !== null || process.signalCode !== null) return true;
@@ -1262,20 +1409,55 @@ var Browser = class {
1262
1409
  throw error;
1263
1410
  }
1264
1411
  }
1265
- async #terminate(process) {
1266
- 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;
1267
1422
  try {
1268
- process.kill("SIGTERM");
1269
- } catch {
1270
- 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
+ });
1271
1434
  }
1272
- if (await this.#waitForProcessWithin(process, 3e3)) return;
1273
- try {
1274
- process.kill("SIGKILL");
1275
- } catch {
1276
- 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)));
1277
1446
  }
1278
- 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 });
1279
1461
  }
1280
1462
  #finish() {
1281
1463
  this.#unbindTransport();
@@ -1286,6 +1468,7 @@ var Browser = class {
1286
1468
  this.#endpoint = void 0;
1287
1469
  this.#client = void 0;
1288
1470
  this.#process = void 0;
1471
+ this.#exitCleanup = void 0;
1289
1472
  this.#contexts = [];
1290
1473
  this.#emitter.emit("destroy");
1291
1474
  this.#emitter.destroy();
@@ -1306,6 +1489,7 @@ exports.BROWSER_KILL_GRACE_MS = BROWSER_KILL_GRACE_MS;
1306
1489
  exports.BROWSER_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS;
1307
1490
  exports.BROWSER_PORT_PROBE_TIMEOUT_MS = BROWSER_PORT_PROBE_TIMEOUT_MS;
1308
1491
  exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
1492
+ exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
1309
1493
  exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
1310
1494
  exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
1311
1495
  exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
@@ -1322,6 +1506,7 @@ exports.BrowserNotConnectedError = BrowserNotConnectedError;
1322
1506
  exports.WebSocketCDPTransport = WebSocketCDPTransport;
1323
1507
  exports.browserToEngine = browserToEngine;
1324
1508
  exports.createBrowser = createBrowser;
1509
+ exports.createBrowserProfile = createBrowserProfile;
1325
1510
  exports.createCDPTransport = createCDPTransport;
1326
1511
  exports.createScreenshotWriter = createScreenshotWriter;
1327
1512
  exports.defaultInstallPaths = defaultInstallPaths;
@@ -1343,6 +1528,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
1343
1528
  exports.parseBrowserEngine = parseBrowserEngine;
1344
1529
  exports.probeAllPathNames = probeAllPathNames;
1345
1530
  exports.probePathNames = probePathNames;
1531
+ exports.removeBrowserProfile = removeBrowserProfile;
1346
1532
  exports.waitForCDPReady = waitForCDPReady;
1347
1533
  exports.windowsRoots = windowsRoots;
1348
1534