@ricsam/r5d-browser 0.0.54 → 0.0.56

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.
@@ -35,6 +35,7 @@ __export(chrome_launcher_exports, {
35
35
  defaultChromeProfilePath: () => defaultChromeProfilePath,
36
36
  launchStableChrome: () => launchStableChrome,
37
37
  resolveChromeExecutable: () => resolveChromeExecutable,
38
+ terminateOwnedProcess: () => terminateOwnedProcess,
38
39
  validateChromeProfilePath: () => validateChromeProfilePath,
39
40
  validateDebuggerWebSocketUrl: () => validateDebuggerWebSocketUrl,
40
41
  validateEndpointOwnership: () => validateEndpointOwnership
@@ -529,21 +530,29 @@ async function requestOwnedBrowserClose(webSocketDebuggerUrl, chromePid) {
529
530
  cdp.close();
530
531
  }
531
532
  }
532
- async function terminateOwnedProcess(params) {
533
+ async function terminateOwnedProcess(params, timeouts = {
534
+ gracefulCloseMs: GRACEFUL_CLOSE_TIMEOUT_MS,
535
+ terminateMs: TERMINATE_TIMEOUT_MS
536
+ }) {
533
537
  if (params.child.exitCode !== null || params.child.signalCode !== null) return;
534
538
  if (params.browser?.isConnected()) {
535
539
  void params.browser.newBrowserCDPSession().then(async (cdp) => {
536
540
  await cdp.send("Browser.close");
537
541
  }).catch(() => void 0);
538
- if (await waitForExit(params.exited, GRACEFUL_CLOSE_TIMEOUT_MS)) return;
542
+ if (await waitForExit(params.exited, timeouts.gracefulCloseMs)) return;
539
543
  } else if (params.ownedWebSocketDebuggerUrl) {
540
544
  const closeRequested = await requestOwnedBrowserClose(params.ownedWebSocketDebuggerUrl, params.chromePid).catch(() => false);
541
- if (closeRequested && await waitForExit(params.exited, GRACEFUL_CLOSE_TIMEOUT_MS)) return;
545
+ if (closeRequested && await waitForExit(params.exited, timeouts.gracefulCloseMs)) return;
542
546
  }
543
547
  if (params.child.exitCode === null && params.child.signalCode === null) params.child.kill("SIGTERM");
544
- if (await waitForExit(params.exited, TERMINATE_TIMEOUT_MS)) return;
548
+ if (await waitForExit(params.exited, timeouts.terminateMs)) return;
545
549
  if (params.child.exitCode === null && params.child.signalCode === null) params.child.kill("SIGKILL");
546
- await waitForExit(params.exited, TERMINATE_TIMEOUT_MS);
550
+ if (await waitForExit(params.exited, timeouts.terminateMs)) return;
551
+ const stderr = params.stderr?.().trim();
552
+ throw new Error(
553
+ `Google Chrome PID ${params.chromePid} did not exit within ${timeouts.terminateMs}ms after SIGKILL; the run lock was retained to protect its profile.${stderr ? ` Chrome stderr:
554
+ ${stderr}` : ""}`
555
+ );
547
556
  }
548
557
  function occupiedProfileError(profilePath) {
549
558
  const owner = singletonLockOwner(profilePath);
@@ -562,6 +571,7 @@ async function launchStableChrome(options) {
562
571
  const profilePath = validateChromeProfilePath(options.profilePath);
563
572
  const playwrightDownloadsPath = import_node_path.default.resolve(options.playwrightDownloadsPath);
564
573
  const lock = acquireChromeRunLock(options.lockPath ?? defaultChromeLockPath(), profilePath);
574
+ let releaseLockOnError = true;
565
575
  try {
566
576
  import_node_fs.default.mkdirSync(profilePath, { recursive: true, mode: 448 });
567
577
  import_node_fs.default.mkdirSync(playwrightDownloadsPath, { recursive: true, mode: 448 });
@@ -618,17 +628,20 @@ async function launchStableChrome(options) {
618
628
  let closePromise;
619
629
  const close = () => {
620
630
  closePromise ??= (async () => {
631
+ let processExited = false;
621
632
  try {
622
633
  await terminateOwnedProcess({
623
634
  child,
624
635
  exited: observed.exited,
625
636
  chromePid,
626
637
  ownedWebSocketDebuggerUrl,
627
- browser
638
+ browser,
639
+ stderr: observed.stderr
628
640
  });
641
+ processExited = true;
629
642
  await browser.close().catch(() => void 0);
630
643
  } finally {
631
- lock.release();
644
+ if (processExited) lock.release();
632
645
  }
633
646
  })();
634
647
  return closePromise;
@@ -650,7 +663,11 @@ async function launchStableChrome(options) {
650
663
  child,
651
664
  exited: observed.exited,
652
665
  chromePid,
653
- ownedWebSocketDebuggerUrl
666
+ ownedWebSocketDebuggerUrl,
667
+ stderr: observed.stderr
668
+ }).catch((terminationError) => {
669
+ releaseLockOnError = false;
670
+ throw terminationError;
654
671
  });
655
672
  const occupied = occupiedProfileError(profilePath);
656
673
  if (occupied) throw occupied;
@@ -661,7 +678,7 @@ async function launchStableChrome(options) {
661
678
  `Could not launch Google Chrome after ${MAX_LAUNCH_ATTEMPTS} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
662
679
  );
663
680
  } catch (error) {
664
- lock.release();
681
+ if (releaseLockOnError) lock.release();
665
682
  throw error;
666
683
  }
667
684
  }
@@ -674,6 +691,7 @@ async function launchStableChrome(options) {
674
691
  defaultChromeProfilePath,
675
692
  launchStableChrome,
676
693
  resolveChromeExecutable,
694
+ terminateOwnedProcess,
677
695
  validateChromeProfilePath,
678
696
  validateDebuggerWebSocketUrl,
679
697
  validateEndpointOwnership
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "type": "commonjs"
5
5
  }
@@ -487,21 +487,29 @@ async function requestOwnedBrowserClose(webSocketDebuggerUrl, chromePid) {
487
487
  cdp.close();
488
488
  }
489
489
  }
490
- async function terminateOwnedProcess(params) {
490
+ async function terminateOwnedProcess(params, timeouts = {
491
+ gracefulCloseMs: GRACEFUL_CLOSE_TIMEOUT_MS,
492
+ terminateMs: TERMINATE_TIMEOUT_MS
493
+ }) {
491
494
  if (params.child.exitCode !== null || params.child.signalCode !== null) return;
492
495
  if (params.browser?.isConnected()) {
493
496
  void params.browser.newBrowserCDPSession().then(async (cdp) => {
494
497
  await cdp.send("Browser.close");
495
498
  }).catch(() => void 0);
496
- if (await waitForExit(params.exited, GRACEFUL_CLOSE_TIMEOUT_MS)) return;
499
+ if (await waitForExit(params.exited, timeouts.gracefulCloseMs)) return;
497
500
  } else if (params.ownedWebSocketDebuggerUrl) {
498
501
  const closeRequested = await requestOwnedBrowserClose(params.ownedWebSocketDebuggerUrl, params.chromePid).catch(() => false);
499
- if (closeRequested && await waitForExit(params.exited, GRACEFUL_CLOSE_TIMEOUT_MS)) return;
502
+ if (closeRequested && await waitForExit(params.exited, timeouts.gracefulCloseMs)) return;
500
503
  }
501
504
  if (params.child.exitCode === null && params.child.signalCode === null) params.child.kill("SIGTERM");
502
- if (await waitForExit(params.exited, TERMINATE_TIMEOUT_MS)) return;
505
+ if (await waitForExit(params.exited, timeouts.terminateMs)) return;
503
506
  if (params.child.exitCode === null && params.child.signalCode === null) params.child.kill("SIGKILL");
504
- await waitForExit(params.exited, TERMINATE_TIMEOUT_MS);
507
+ if (await waitForExit(params.exited, timeouts.terminateMs)) return;
508
+ const stderr = params.stderr?.().trim();
509
+ throw new Error(
510
+ `Google Chrome PID ${params.chromePid} did not exit within ${timeouts.terminateMs}ms after SIGKILL; the run lock was retained to protect its profile.${stderr ? ` Chrome stderr:
511
+ ${stderr}` : ""}`
512
+ );
505
513
  }
506
514
  function occupiedProfileError(profilePath) {
507
515
  const owner = singletonLockOwner(profilePath);
@@ -520,6 +528,7 @@ async function launchStableChrome(options) {
520
528
  const profilePath = validateChromeProfilePath(options.profilePath);
521
529
  const playwrightDownloadsPath = path.resolve(options.playwrightDownloadsPath);
522
530
  const lock = acquireChromeRunLock(options.lockPath ?? defaultChromeLockPath(), profilePath);
531
+ let releaseLockOnError = true;
523
532
  try {
524
533
  fs.mkdirSync(profilePath, { recursive: true, mode: 448 });
525
534
  fs.mkdirSync(playwrightDownloadsPath, { recursive: true, mode: 448 });
@@ -576,17 +585,20 @@ async function launchStableChrome(options) {
576
585
  let closePromise;
577
586
  const close = () => {
578
587
  closePromise ??= (async () => {
588
+ let processExited = false;
579
589
  try {
580
590
  await terminateOwnedProcess({
581
591
  child,
582
592
  exited: observed.exited,
583
593
  chromePid,
584
594
  ownedWebSocketDebuggerUrl,
585
- browser
595
+ browser,
596
+ stderr: observed.stderr
586
597
  });
598
+ processExited = true;
587
599
  await browser.close().catch(() => void 0);
588
600
  } finally {
589
- lock.release();
601
+ if (processExited) lock.release();
590
602
  }
591
603
  })();
592
604
  return closePromise;
@@ -608,7 +620,11 @@ async function launchStableChrome(options) {
608
620
  child,
609
621
  exited: observed.exited,
610
622
  chromePid,
611
- ownedWebSocketDebuggerUrl
623
+ ownedWebSocketDebuggerUrl,
624
+ stderr: observed.stderr
625
+ }).catch((terminationError) => {
626
+ releaseLockOnError = false;
627
+ throw terminationError;
612
628
  });
613
629
  const occupied = occupiedProfileError(profilePath);
614
630
  if (occupied) throw occupied;
@@ -619,7 +635,7 @@ async function launchStableChrome(options) {
619
635
  `Could not launch Google Chrome after ${MAX_LAUNCH_ATTEMPTS} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
620
636
  );
621
637
  } catch (error) {
622
- lock.release();
638
+ if (releaseLockOnError) lock.release();
623
639
  throw error;
624
640
  }
625
641
  }
@@ -631,6 +647,7 @@ export {
631
647
  defaultChromeProfilePath,
632
648
  launchStableChrome,
633
649
  resolveChromeExecutable,
650
+ terminateOwnedProcess,
634
651
  validateChromeProfilePath,
635
652
  validateDebuggerWebSocketUrl,
636
653
  validateEndpointOwnership
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "type": "module"
5
5
  }
@@ -1,3 +1,4 @@
1
+ import { type ChildProcess } from "node:child_process";
1
2
  import { type Browser, type BrowserContext } from "playwright-core";
2
3
  export type ChromeExit = {
3
4
  code: number | null;
@@ -26,6 +27,10 @@ export type ChromeRunLock = {
26
27
  token: string;
27
28
  release(): void;
28
29
  };
30
+ type ChromeTerminationTimeouts = {
31
+ gracefulCloseMs: number;
32
+ terminateMs: number;
33
+ };
29
34
  export declare function defaultChromeProfilePath(homeDirectory?: string): string;
30
35
  export declare function defaultChromeLockPath(homeDirectory?: string): string;
31
36
  export declare function resolveChromeExecutable(options?: {
@@ -50,4 +55,14 @@ export declare function validateEndpointOwnership(params: {
50
55
  product: unknown;
51
56
  commandLine: unknown;
52
57
  }): string;
58
+ /** @internal Exported only so the bounded shutdown failure can be unit-tested. */
59
+ export declare function terminateOwnedProcess(params: {
60
+ child: ChildProcess;
61
+ exited: Promise<ChromeExit>;
62
+ chromePid: number;
63
+ ownedWebSocketDebuggerUrl?: string;
64
+ browser?: Browser;
65
+ stderr?: () => string;
66
+ }, timeouts?: ChromeTerminationTimeouts): Promise<void>;
53
67
  export declare function launchStableChrome(options: LaunchStableChromeOptions): Promise<StableChromeSession>;
68
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",