@narumitw/pi-chrome-devtools 0.54.0 → 0.54.1

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.
@@ -484,7 +484,6 @@ var DEFAULT_HTTP_TIMEOUT_MS = 1e3;
484
484
  var DEFAULT_ENDPOINT_WAIT_MS = 5e3;
485
485
  var DEFAULT_ENDPOINT_RETRY_MS = 250;
486
486
  var MANAGED_BROWSER_PROFILE_PREFIX = "pi-chrome-devtools-profile-";
487
- var DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
488
487
  var BROWSER_SHUTDOWN_WAIT_MS = 1500;
489
488
  var state = {
490
489
  host: DEFAULT_HOST,
@@ -589,15 +588,265 @@ function applyRuntimeBrowserSettings(browser, paths, projectTrusted) {
589
588
 
590
589
  // src/browser-manager.ts
591
590
  import { execFile, spawn } from "node:child_process";
592
- import { constants as constants2 } from "node:fs";
593
- import { access as access2, mkdtemp, readFile as readFile2, rm as rm2 } from "node:fs/promises";
591
+ import { access as access2, mkdtemp, rm as rm2 } from "node:fs/promises";
594
592
  import { createServer } from "node:net";
595
593
  import { tmpdir } from "node:os";
594
+ import { join as join3 } from "node:path";
595
+
596
+ // src/browser-candidates.ts
597
+ import { constants as constants2 } from "node:fs";
596
598
  import { delimiter, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
599
+ function normalizePathForComparison(value) {
600
+ return process.platform === "win32" ? value.toLowerCase() : value;
601
+ }
602
+ function browserCandidateDefinitions(browser) {
603
+ const explicitCandidate = explicitBrowserCandidateDefinition(browser);
604
+ if (explicitCandidate.length > 0) return explicitCandidate;
605
+ return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
606
+ }
607
+ function explicitBrowserCandidateDefinition(browser) {
608
+ if (!browser.executablePath) return [];
609
+ return [
610
+ {
611
+ label: browser.executablePathSource === "environment" ? "PI_CHROME_DEVTOOLS_BROWSER" : "browser.executablePath",
612
+ executable: browser.executablePath,
613
+ source: "env"
614
+ }
615
+ ];
616
+ }
617
+ function platformBrowserCandidateDefinitions() {
618
+ if (process.platform === "darwin") {
619
+ return [
620
+ {
621
+ label: "Google Chrome",
622
+ executable: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
623
+ source: "wellKnownPath"
624
+ },
625
+ {
626
+ label: "Chromium",
627
+ executable: "/Applications/Chromium.app/Contents/MacOS/Chromium",
628
+ source: "wellKnownPath"
629
+ },
630
+ {
631
+ label: "Brave Browser",
632
+ executable: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
633
+ source: "wellKnownPath"
634
+ },
635
+ {
636
+ label: "Microsoft Edge",
637
+ executable: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
638
+ source: "wellKnownPath"
639
+ }
640
+ ];
641
+ }
642
+ if (process.platform === "win32") {
643
+ return windowsBrowserCandidateDefinitions();
644
+ }
645
+ return [
646
+ { label: "Google Chrome", executable: "google-chrome", source: "path" },
647
+ { label: "Google Chrome Stable", executable: "google-chrome-stable", source: "path" },
648
+ { label: "Chromium", executable: "chromium", source: "path" },
649
+ { label: "Chromium Browser", executable: "chromium-browser", source: "path" },
650
+ { label: "Brave Browser", executable: "brave-browser", source: "path" },
651
+ { label: "Brave", executable: "brave", source: "path" },
652
+ { label: "Microsoft Edge", executable: "microsoft-edge", source: "path" },
653
+ { label: "Microsoft Edge Stable", executable: "microsoft-edge-stable", source: "path" }
654
+ ];
655
+ }
656
+ function windowsBrowserCandidateDefinitions() {
657
+ const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(
658
+ (value) => typeof value === "string" && value.length > 0
659
+ );
660
+ const wellKnownPaths = programFiles.flatMap((root) => [
661
+ join2(root, "Google", "Chrome", "Application", "chrome.exe"),
662
+ join2(root, "Chromium", "Application", "chrome.exe"),
663
+ join2(root, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
664
+ join2(root, "Microsoft", "Edge", "Application", "msedge.exe")
665
+ ]);
666
+ return [
667
+ ...wellKnownPaths.map((executable) => ({
668
+ label: browserLabelFromExecutable(executable),
669
+ executable,
670
+ source: "wellKnownPath"
671
+ })),
672
+ { label: "Google Chrome", executable: "chrome.exe", source: "path" },
673
+ { label: "Chromium", executable: "chromium.exe", source: "path" },
674
+ { label: "Brave Browser", executable: "brave.exe", source: "path" },
675
+ { label: "Microsoft Edge", executable: "msedge.exe", source: "path" }
676
+ ];
677
+ }
678
+ function browserLabelFromExecutable(executable) {
679
+ const normalizedExecutable = normalizePathForComparison(executable);
680
+ if (normalizedExecutable.includes("brave")) return "Brave Browser";
681
+ if (normalizedExecutable.includes("edge") || normalizedExecutable.includes("msedge")) {
682
+ return "Microsoft Edge";
683
+ }
684
+ if (normalizedExecutable.includes("chromium")) return "Chromium";
685
+ return "Google Chrome";
686
+ }
687
+ function uniqueBrowserCandidates(candidates) {
688
+ const seen = /* @__PURE__ */ new Set();
689
+ return candidates.filter((candidate) => {
690
+ const key = normalizePathForComparison(candidate.executable);
691
+ if (seen.has(key)) return false;
692
+ seen.add(key);
693
+ return true;
694
+ });
695
+ }
696
+ async function resolveBrowserCandidates(definitions, access3, throwIfCancelled) {
697
+ const candidates = [];
698
+ for (const definition of definitions) {
699
+ const resolvedExecutable = await resolveBrowserExecutable(definition.executable, access3, throwIfCancelled);
700
+ throwIfCancelled();
701
+ if (!resolvedExecutable) continue;
702
+ candidates.push({ ...definition, resolvedExecutable });
703
+ }
704
+ return uniqueBrowserCandidatesByResolvedPath(candidates);
705
+ }
706
+ function uniqueBrowserCandidatesByResolvedPath(candidates) {
707
+ const seen = /* @__PURE__ */ new Set();
708
+ return candidates.filter((candidate) => {
709
+ const key = normalizePathForComparison(resolve2(candidate.resolvedExecutable));
710
+ if (seen.has(key)) return false;
711
+ seen.add(key);
712
+ return true;
713
+ });
714
+ }
715
+ async function resolveBrowserExecutable(executable, access3, throwIfCancelled) {
716
+ if (hasPathSeparator(executable) || isAbsolute2(executable)) {
717
+ const resolvedExecutable = isAbsolute2(executable) ? executable : resolve2(executable);
718
+ return await canAccessExecutable(resolvedExecutable, access3, throwIfCancelled) ? resolvedExecutable : void 0;
719
+ }
720
+ for (const directory of executableSearchPath()) {
721
+ for (const executableName of executableSearchNames(executable)) {
722
+ const candidate = join2(directory, executableName);
723
+ if (await canAccessExecutable(candidate, access3, throwIfCancelled)) return candidate;
724
+ }
725
+ }
726
+ return void 0;
727
+ }
728
+ function hasPathSeparator(path) {
729
+ return path.includes("/") || path.includes("\\");
730
+ }
731
+ function executableSearchPath() {
732
+ return (process.env.PATH ?? "").split(delimiter).filter((part) => part.length > 0);
733
+ }
734
+ function executableSearchNames(executable) {
735
+ if (process.platform !== "win32" || /\.[a-z0-9]+$/i.test(executable)) return [executable];
736
+ return [executable, `${executable}.exe`, `${executable}.cmd`, `${executable}.bat`];
737
+ }
738
+ async function canAccessExecutable(path, access3, throwIfCancelled) {
739
+ let accessible = false;
740
+ try {
741
+ await access3(path, process.platform === "win32" ? constants2.F_OK : constants2.X_OK);
742
+ accessible = true;
743
+ } catch {
744
+ }
745
+ throwIfCancelled();
746
+ return accessible;
747
+ }
748
+
749
+ // src/browser-startup.ts
750
+ var MAX_STARTUP_STDERR_BYTES = 64 * 1024;
751
+ function handleErrorsUntilClose(emitter) {
752
+ const ignoreError = () => void 0;
753
+ emitter.on("error", ignoreError);
754
+ emitter.once("close", () => emitter.off("error", ignoreError));
755
+ }
756
+ function browserWebSocketIdentity(value, port) {
757
+ if (typeof value !== "string") return void 0;
758
+ try {
759
+ const url = new URL(value);
760
+ if (url.protocol !== "ws:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || Number(url.port || "80") !== port || url.username || url.password || url.search || url.hash || !/^\/devtools\/browser\/[^/]+$/.test(url.pathname)) {
761
+ return void 0;
762
+ }
763
+ return url.pathname;
764
+ } catch {
765
+ return void 0;
766
+ }
767
+ }
768
+ function waitForBrowserStartup(child, port, waitMs, signal) {
769
+ handleErrorsUntilClose(child);
770
+ const stderr = child.stderr;
771
+ if (stderr && !stderr.closed) handleErrorsUntilClose(stderr);
772
+ signal.throwIfAborted();
773
+ if (!stderr) return Promise.reject(new Error("Auto-launched browser has no startup stderr stream."));
774
+ return new Promise((resolve3, reject) => {
775
+ let spawned = false;
776
+ let identity;
777
+ let receivedBytes = 0;
778
+ let pendingLine = "";
779
+ let settled = false;
780
+ let timeout;
781
+ const settle = (error) => {
782
+ if (settled) return;
783
+ if (!error && (!spawned || !identity)) return;
784
+ settled = true;
785
+ clearTimeout(timeout);
786
+ child.off("spawn", onSpawn);
787
+ child.off("error", onError);
788
+ child.off("exit", onExit);
789
+ child.off("close", onExit);
790
+ stderr.off("data", onData);
791
+ stderr.off("end", onEnd);
792
+ stderr.off("error", onError);
793
+ signal.removeEventListener("abort", onAbort);
794
+ stderr.resume();
795
+ if (error) reject(error);
796
+ else resolve3(identity);
797
+ };
798
+ const onError = (error) => settle(error);
799
+ const onAbort = () => settle(new Error("Chrome DevTools browser startup cancelled.", { cause: signal.reason }));
800
+ const onExit = () => settle(new Error("Auto-launched browser exited before DevTools became available."));
801
+ const onSpawn = () => {
802
+ spawned = true;
803
+ timeout = setTimeout(
804
+ () => settle(new Error("Timed out waiting for auto-launched browser DevTools startup announcement.")),
805
+ waitMs
806
+ );
807
+ settle();
808
+ };
809
+ const inspectLine = (line) => {
810
+ const match = /^DevTools listening on (\S+)$/.exec(line.trim());
811
+ if (!match) return;
812
+ identity = browserWebSocketIdentity(match[1], port);
813
+ if (!identity) settle(new Error("Auto-launched browser announced an unexpected DevTools endpoint."));
814
+ else settle();
815
+ };
816
+ const onData = (data) => {
817
+ receivedBytes += Buffer.byteLength(data);
818
+ if (receivedBytes > MAX_STARTUP_STDERR_BYTES) {
819
+ settle(new Error("Auto-launched browser startup stderr exceeded 64 KB before readiness."));
820
+ return;
821
+ }
822
+ pendingLine += data.toString();
823
+ const lines = pendingLine.split("\n");
824
+ pendingLine = lines.pop() ?? "";
825
+ for (const line of lines) {
826
+ inspectLine(line);
827
+ if (settled) break;
828
+ }
829
+ };
830
+ const onEnd = () => {
831
+ inspectLine(pendingLine);
832
+ if (!identity) settle(new Error("Auto-launched browser closed stderr before its DevTools startup announcement."));
833
+ };
834
+ child.once("spawn", onSpawn);
835
+ child.once("error", onError);
836
+ child.once("exit", onExit);
837
+ child.once("close", onExit);
838
+ stderr.on("data", onData);
839
+ stderr.once("end", onEnd);
840
+ stderr.once("error", onError);
841
+ signal.addEventListener("abort", onAbort, { once: true });
842
+ });
843
+ }
844
+
845
+ // src/browser-manager.ts
597
846
  var DEFAULT_BROWSER_MANAGER_OPERATIONS = {
598
847
  access: access2,
599
848
  mkdtemp,
600
- readFile: readFile2,
849
+ allocatePort: allocateBrowserPort,
601
850
  rm: (path, options) => rm2(path, options),
602
851
  fetch: (input, init) => fetch(input, init),
603
852
  spawn,
@@ -670,9 +919,13 @@ function setManagedBrowser(owner, browser) {
670
919
  function launchPromiseForOwner(owner) {
671
920
  return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
672
921
  }
922
+ function latestLaunchPromiseForOwner(owner) {
923
+ return owner ? managedBrowserScope(owner).latestLaunchPromise : state.latestLaunchPromise;
924
+ }
673
925
  function setLaunchPromise(owner, launchPromise) {
674
- if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
675
- else state.launchPromise = launchPromise;
926
+ const scope = owner ? managedBrowserScope(owner) : state;
927
+ scope.launchPromise = launchPromise;
928
+ if (launchPromise) scope.latestLaunchPromise = launchPromise;
676
929
  }
677
930
  function managedBrowserPort(owner) {
678
931
  return owner ? managedBrowserScope(owner).port : state.port;
@@ -697,12 +950,6 @@ function setLastLaunchAttempt(owner, attempt) {
697
950
  function formatError2(error) {
698
951
  return error instanceof Error ? error.message : String(error);
699
952
  }
700
- function isNodeError2(error) {
701
- return error instanceof Error && "code" in error;
702
- }
703
- function normalizePathForComparison(value) {
704
- return process.platform === "win32" ? value.toLowerCase() : value;
705
- }
706
953
  async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS, callerSignal, owner) {
707
954
  callerSignal?.throwIfAborted();
708
955
  if (extensionsConfigured(owner)) {
@@ -710,14 +957,19 @@ async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS, callerS
710
957
  await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
711
958
  return;
712
959
  }
960
+ const pendingLaunch = launchPromiseForOwner(owner);
961
+ if (pendingLaunch) {
962
+ await waitForCaller(pendingLaunch, callerSignal);
963
+ return;
964
+ }
713
965
  if (canAutoLaunchBrowser(owner)) {
966
+ const launchBeforeProbe = latestLaunchPromiseForOwner(owner);
714
967
  try {
715
968
  await withEndpointRetry(
716
969
  () => fetchDevToolsJson("/json/version", { signal: callerSignal }, owner),
717
970
  waitMs,
718
971
  callerSignal
719
972
  );
720
- return;
721
973
  } catch (error) {
722
974
  if (shouldAutoLaunchAfterEndpointError(error, owner)) {
723
975
  await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
@@ -725,6 +977,14 @@ async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS, callerS
725
977
  }
726
978
  throw error;
727
979
  }
980
+ let observedLaunch = launchBeforeProbe;
981
+ while (true) {
982
+ const concurrentLaunch = latestLaunchPromiseForOwner(owner);
983
+ if (!concurrentLaunch || concurrentLaunch === observedLaunch) break;
984
+ observedLaunch = concurrentLaunch;
985
+ await waitForCaller(concurrentLaunch, callerSignal);
986
+ }
987
+ return;
728
988
  }
729
989
  try {
730
990
  await fetchDevToolsJson("/json/version", { signal: callerSignal }, owner);
@@ -802,7 +1062,7 @@ async function prepareManagedBrowserLaunch(waitMs, generation, signal, owner) {
802
1062
  throwIfBrowserLaunchCancelled(generation, signal, owner);
803
1063
  if (!available) {
804
1064
  throw new DevToolsEndpointError(
805
- `Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`
1065
+ `Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use an automatically selected managed port.`
806
1066
  );
807
1067
  }
808
1068
  }
@@ -810,12 +1070,16 @@ async function prepareManagedBrowserLaunch(waitMs, generation, signal, owner) {
810
1070
  }
811
1071
  async function launchManagedBrowser(waitMs, generation, signal, owner) {
812
1072
  throwIfBrowserLaunchCancelled(generation, signal, owner);
813
- const candidateDefinitions = browserCandidateDefinitions(owner);
814
- const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal, owner);
1073
+ const candidateDefinitions = browserCandidateDefinitions(browserSettingsForOwner(owner));
1074
+ const candidates = await resolveBrowserCandidates(
1075
+ candidateDefinitions,
1076
+ browserManagerOperations.access,
1077
+ () => throwIfBrowserLaunchCancelled(generation, signal, owner)
1078
+ );
815
1079
  throwIfBrowserLaunchCancelled(generation, signal, owner);
816
1080
  setLastLaunchAttempt(owner, {
817
1081
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
818
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
1082
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "automatic-port"
819
1083
  });
820
1084
  if (candidates.length === 0) {
821
1085
  throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
@@ -830,7 +1094,7 @@ async function launchManagedBrowser(waitMs, generation, signal, owner) {
830
1094
  setLastLaunchAttempt(owner, {
831
1095
  ...lastLaunchAttempt(owner),
832
1096
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
833
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
1097
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "automatic-port",
834
1098
  selectedCandidate: formatBrowserCandidate(candidate),
835
1099
  userDataDir: managedBrowserForOwner(owner)?.userDataDir
836
1100
  });
@@ -842,7 +1106,7 @@ async function launchManagedBrowser(waitMs, generation, signal, owner) {
842
1106
  setLastLaunchAttempt(owner, {
843
1107
  ...lastLaunchAttempt(owner) ?? {
844
1108
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
845
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
1109
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "automatic-port"
846
1110
  },
847
1111
  lastError: formatError2(cancellation)
848
1112
  });
@@ -852,7 +1116,7 @@ async function launchManagedBrowser(waitMs, generation, signal, owner) {
852
1116
  setLastLaunchAttempt(owner, {
853
1117
  ...lastLaunchAttempt(owner) ?? {
854
1118
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
855
- mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
1119
+ mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "automatic-port"
856
1120
  },
857
1121
  lastError: formatError2(error)
858
1122
  });
@@ -897,19 +1161,24 @@ function classifyExtensionBrowserVersion(version) {
897
1161
  }
898
1162
  async function launchBrowserCandidate(candidate, waitMs, generation, signal, owner) {
899
1163
  throwIfBrowserLaunchCancelled(generation, signal, owner);
900
- const userDataDir = await browserManagerOperations.mkdtemp(join2(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX));
1164
+ const userDataDir = await browserManagerOperations.mkdtemp(join3(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX));
901
1165
  let managedBrowser;
902
1166
  try {
903
1167
  throwIfBrowserLaunchCancelled(generation, signal, owner);
904
- const portArgument = managedBrowserPortConfigured(owner) ? String(managedBrowserPort(owner)) : "0";
905
- const args = buildManagedBrowserLaunchArguments(userDataDir, portArgument, managedBrowserExtensionPaths(owner));
1168
+ const port = managedBrowserPortConfigured(owner) ? managedBrowserPort(owner) : await browserManagerOperations.allocatePort(browserSettingsForOwner(owner).host);
1169
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
1170
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1171
+ throw new DevToolsEndpointError("Cannot launch a browser without a valid nonzero DevTools port.");
1172
+ }
1173
+ const args = buildManagedBrowserLaunchArguments(userDataDir, String(port), managedBrowserExtensionPaths(owner));
906
1174
  const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
907
1175
  shell: false,
908
- stdio: "ignore"
1176
+ stdio: ["ignore", "ignore", "pipe"]
909
1177
  });
910
1178
  const launchedBrowser = {
911
1179
  process: child,
912
1180
  userDataDir,
1181
+ port,
913
1182
  exited: false,
914
1183
  ready: false,
915
1184
  ownerGeneration: generation,
@@ -927,24 +1196,12 @@ async function launchBrowserCandidate(candidate, waitMs, generation, signal, own
927
1196
  setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
928
1197
  }
929
1198
  });
930
- await waitForBrowserSpawn(child);
1199
+ const browserIdentity = await waitForBrowserStartup(child, port, waitMs, signal);
931
1200
  throwIfBrowserLaunchCancelled(generation, signal, owner);
932
- if (managedBrowserPortConfigured(owner)) {
933
- launchedBrowser.port = managedBrowserPort(owner);
934
- } else {
935
- launchedBrowser.port = await readManagedBrowserPort(
936
- userDataDir,
937
- launchedBrowser,
938
- waitMs,
939
- generation,
940
- signal,
941
- owner
942
- );
943
- throwIfBrowserLaunchCancelled(generation, signal, owner);
944
- setManagedBrowserPort(owner, launchedBrowser.port);
945
- }
946
- await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
1201
+ setManagedBrowserPort(owner, port);
1202
+ await waitForDevToolsEndpoint(waitMs, launchedBrowser, browserIdentity, generation, signal, owner);
947
1203
  throwIfBrowserLaunchCancelled(generation, signal, owner);
1204
+ throwIfManagedBrowserExited(launchedBrowser);
948
1205
  launchedBrowser.ready = true;
949
1206
  } catch (error) {
950
1207
  if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
@@ -969,55 +1226,21 @@ function buildManagedBrowserLaunchArguments(userDataDir, portArgument, extension
969
1226
  "about:blank"
970
1227
  ];
971
1228
  }
972
- function waitForBrowserSpawn(child) {
973
- return new Promise((resolve3, reject) => {
974
- let settled = false;
975
- const settle = (callback) => {
976
- if (settled) return;
977
- settled = true;
978
- child.off("error", onError);
979
- child.off("spawn", onSpawn);
980
- callback();
981
- };
982
- const onError = (error) => settle(() => reject(error));
983
- const onSpawn = () => settle(resolve3);
984
- child.once("error", onError);
985
- child.once("spawn", onSpawn);
986
- });
987
- }
988
- async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal, owner) {
989
- const activePortFile = join2(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
990
- const deadline = Date.now() + waitMs;
991
- while (true) {
992
- throwIfManagedBrowserExited(managedBrowser);
993
- const text = await browserManagerOperations.readFile(activePortFile, "utf8").catch((error) => {
994
- if (isNodeError2(error) && error.code === "ENOENT") return void 0;
995
- throw error;
996
- });
997
- throwIfBrowserLaunchCancelled(generation, signal, owner);
998
- const portText = text?.split(/\r?\n/, 1)[0]?.trim();
999
- const port = Number(portText);
1000
- if (Number.isInteger(port) && port > 0) return port;
1001
- const remainingMs = deadline - Date.now();
1002
- if (remainingMs <= 0) {
1003
- throw new DevToolsEndpointError(
1004
- [
1005
- "Timed out waiting for auto-launched browser DevToolsActivePort.",
1006
- `Expected file: ${activePortFile}`,
1007
- launchHint(owner)
1008
- ].join("\n")
1009
- );
1010
- }
1011
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
1012
- }
1013
- }
1014
- async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal, owner) {
1229
+ async function waitForDevToolsEndpoint(waitMs, managedBrowser, browserIdentity, generation, signal, owner) {
1015
1230
  const deadline = Date.now() + waitMs;
1016
1231
  while (true) {
1017
1232
  throwIfManagedBrowserExited(managedBrowser);
1018
1233
  try {
1019
- await fetchDevToolsJson("/json/version", { signal }, owner);
1234
+ const version = await fetchDevToolsJson(
1235
+ "/json/version",
1236
+ { signal },
1237
+ owner
1238
+ );
1020
1239
  throwIfBrowserLaunchCancelled(generation, signal, owner);
1240
+ throwIfManagedBrowserExited(managedBrowser);
1241
+ if (browserWebSocketIdentity(version?.webSocketDebuggerUrl, managedBrowser.port ?? 0) !== browserIdentity) {
1242
+ throw new DevToolsEndpointError("DevTools endpoint does not match the auto-launched browser.");
1243
+ }
1021
1244
  return;
1022
1245
  } catch (error) {
1023
1246
  if (!isRetryableEndpointError(error)) throw error;
@@ -1216,16 +1439,16 @@ function launchModeLabel(owner) {
1216
1439
  if (!isLocalDevToolsHost(browser.host)) return "invalid; extensions require a local endpoint";
1217
1440
  if (!browser.autoLaunchEnabled) return "invalid; extensions require auto-launch";
1218
1441
  if (managedBrowser && !managedBrowser.exited) {
1219
- return managedBrowserPortConfigured(owner) ? "extension-owned browser on explicit port" : "extension-owned browser on dynamic port";
1442
+ return managedBrowserPortConfigured(owner) ? "extension-owned browser on explicit port" : "extension-owned browser on automatically selected port";
1220
1443
  }
1221
- return managedBrowserPortConfigured(owner) ? "force extension-owned launch on explicit port" : "force extension-owned launch on dynamic port";
1444
+ return managedBrowserPortConfigured(owner) ? "force extension-owned launch on explicit port" : "force extension-owned launch on automatically selected port";
1222
1445
  }
1223
1446
  if (!isLocalDevToolsHost(browser.host)) return "manual remote endpoint";
1224
1447
  if (!browser.autoLaunchEnabled) return "manual; auto-launch disabled";
1225
1448
  if (managedBrowser && !managedBrowser.exited) {
1226
- return managedBrowserPortConfigured(owner) ? "auto-launched on explicit port" : "auto-launched on dynamic port";
1449
+ return managedBrowserPortConfigured(owner) ? "auto-launched on explicit port" : "auto-launched on automatically selected port";
1227
1450
  }
1228
- return managedBrowserPortConfigured(owner) ? "attach first; auto-launch explicit port" : "attach first; auto-launch dynamic port";
1451
+ return managedBrowserPortConfigured(owner) ? "attach first; auto-launch explicit port" : "attach first; auto-launch on an automatically selected port";
1229
1452
  }
1230
1453
  function launchAttemptLines(owner) {
1231
1454
  const attempt = lastLaunchAttempt(owner);
@@ -1251,14 +1474,14 @@ function launchHint(owner) {
1251
1474
  if (!browser.autoLaunchEnabled) {
1252
1475
  return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
1253
1476
  }
1254
- const managedMode = managedBrowserPortConfigured(owner) ? `port ${managedBrowserPort(owner)}` : "a dynamic DevTools port";
1477
+ const managedMode = managedBrowserPortConfigured(owner) ? `port ${managedBrowserPort(owner)}` : "an automatically selected nonzero DevTools port";
1255
1478
  return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand(owner)}`;
1256
1479
  }
1257
1480
  function browserCandidateHint(owner) {
1258
1481
  if (extensionsConfigured(owner)) {
1259
1482
  return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
1260
1483
  }
1261
- return `Browser candidates: ${browserCandidateDefinitions(owner).map((candidate) => candidate.label).join(", ")}`;
1484
+ return `Browser candidates: ${browserCandidateDefinitions(browserSettingsForOwner(owner)).map((candidate) => candidate.label).join(", ")}`;
1262
1485
  }
1263
1486
  function chromeLaunchCommand(owner) {
1264
1487
  const executable = browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
@@ -1278,151 +1501,6 @@ function isLocalDevToolsHost(host) {
1278
1501
  const normalizedHost = host.toLowerCase().replace(/^\[(.*)]$/, "$1");
1279
1502
  return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
1280
1503
  }
1281
- function browserCandidateDefinitions(owner) {
1282
- const explicitCandidate = explicitBrowserCandidateDefinition(owner);
1283
- if (explicitCandidate.length > 0) return explicitCandidate;
1284
- return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
1285
- }
1286
- function explicitBrowserCandidateDefinition(owner) {
1287
- const browser = browserSettingsForOwner(owner);
1288
- if (!browser.executablePath) return [];
1289
- return [
1290
- {
1291
- label: browser.executablePathSource === "environment" ? "PI_CHROME_DEVTOOLS_BROWSER" : "browser.executablePath",
1292
- executable: browser.executablePath,
1293
- source: "env"
1294
- }
1295
- ];
1296
- }
1297
- function platformBrowserCandidateDefinitions() {
1298
- if (process.platform === "darwin") {
1299
- return [
1300
- {
1301
- label: "Google Chrome",
1302
- executable: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1303
- source: "wellKnownPath"
1304
- },
1305
- {
1306
- label: "Chromium",
1307
- executable: "/Applications/Chromium.app/Contents/MacOS/Chromium",
1308
- source: "wellKnownPath"
1309
- },
1310
- {
1311
- label: "Brave Browser",
1312
- executable: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
1313
- source: "wellKnownPath"
1314
- },
1315
- {
1316
- label: "Microsoft Edge",
1317
- executable: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
1318
- source: "wellKnownPath"
1319
- }
1320
- ];
1321
- }
1322
- if (process.platform === "win32") {
1323
- return windowsBrowserCandidateDefinitions();
1324
- }
1325
- return [
1326
- { label: "Google Chrome", executable: "google-chrome", source: "path" },
1327
- { label: "Google Chrome Stable", executable: "google-chrome-stable", source: "path" },
1328
- { label: "Chromium", executable: "chromium", source: "path" },
1329
- { label: "Chromium Browser", executable: "chromium-browser", source: "path" },
1330
- { label: "Brave Browser", executable: "brave-browser", source: "path" },
1331
- { label: "Brave", executable: "brave", source: "path" },
1332
- { label: "Microsoft Edge", executable: "microsoft-edge", source: "path" },
1333
- { label: "Microsoft Edge Stable", executable: "microsoft-edge-stable", source: "path" }
1334
- ];
1335
- }
1336
- function windowsBrowserCandidateDefinitions() {
1337
- const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(
1338
- (value) => typeof value === "string" && value.length > 0
1339
- );
1340
- const wellKnownPaths = programFiles.flatMap((root) => [
1341
- join2(root, "Google", "Chrome", "Application", "chrome.exe"),
1342
- join2(root, "Chromium", "Application", "chrome.exe"),
1343
- join2(root, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
1344
- join2(root, "Microsoft", "Edge", "Application", "msedge.exe")
1345
- ]);
1346
- return [
1347
- ...wellKnownPaths.map((executable) => ({
1348
- label: browserLabelFromExecutable(executable),
1349
- executable,
1350
- source: "wellKnownPath"
1351
- })),
1352
- { label: "Google Chrome", executable: "chrome.exe", source: "path" },
1353
- { label: "Chromium", executable: "chromium.exe", source: "path" },
1354
- { label: "Brave Browser", executable: "brave.exe", source: "path" },
1355
- { label: "Microsoft Edge", executable: "msedge.exe", source: "path" }
1356
- ];
1357
- }
1358
- function browserLabelFromExecutable(executable) {
1359
- const normalizedExecutable = normalizePathForComparison(executable);
1360
- if (normalizedExecutable.includes("brave")) return "Brave Browser";
1361
- if (normalizedExecutable.includes("edge") || normalizedExecutable.includes("msedge")) {
1362
- return "Microsoft Edge";
1363
- }
1364
- if (normalizedExecutable.includes("chromium")) return "Chromium";
1365
- return "Google Chrome";
1366
- }
1367
- function uniqueBrowserCandidates(candidates) {
1368
- const seen = /* @__PURE__ */ new Set();
1369
- return candidates.filter((candidate) => {
1370
- const key = normalizePathForComparison(candidate.executable);
1371
- if (seen.has(key)) return false;
1372
- seen.add(key);
1373
- return true;
1374
- });
1375
- }
1376
- async function resolveBrowserCandidates(definitions, generation, signal, owner) {
1377
- const candidates = [];
1378
- for (const definition of definitions) {
1379
- const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
1380
- throwIfBrowserLaunchCancelled(generation, signal, owner);
1381
- if (!resolvedExecutable) continue;
1382
- candidates.push({ ...definition, resolvedExecutable });
1383
- }
1384
- return uniqueBrowserCandidatesByResolvedPath(candidates);
1385
- }
1386
- function uniqueBrowserCandidatesByResolvedPath(candidates) {
1387
- const seen = /* @__PURE__ */ new Set();
1388
- return candidates.filter((candidate) => {
1389
- const key = normalizePathForComparison(resolve2(candidate.resolvedExecutable));
1390
- if (seen.has(key)) return false;
1391
- seen.add(key);
1392
- return true;
1393
- });
1394
- }
1395
- async function resolveBrowserExecutable(executable) {
1396
- if (hasPathSeparator(executable) || isAbsolute2(executable)) {
1397
- const resolvedExecutable = isAbsolute2(executable) ? executable : resolve2(executable);
1398
- return await canAccessExecutable(resolvedExecutable) ? resolvedExecutable : void 0;
1399
- }
1400
- for (const directory of executableSearchPath()) {
1401
- for (const executableName of executableSearchNames(executable)) {
1402
- const candidate = join2(directory, executableName);
1403
- if (await canAccessExecutable(candidate)) return candidate;
1404
- }
1405
- }
1406
- return void 0;
1407
- }
1408
- function hasPathSeparator(path) {
1409
- return path.includes("/") || path.includes("\\");
1410
- }
1411
- function executableSearchPath() {
1412
- return (process.env.PATH ?? "").split(delimiter).filter((part) => part.length > 0);
1413
- }
1414
- function executableSearchNames(executable) {
1415
- if (process.platform !== "win32" || /\.[a-z0-9]+$/i.test(executable)) return [executable];
1416
- return [executable, `${executable}.exe`, `${executable}.cmd`, `${executable}.bat`];
1417
- }
1418
- async function canAccessExecutable(path) {
1419
- try {
1420
- await browserManagerOperations.access(path, process.platform === "win32" ? constants2.F_OK : constants2.X_OK);
1421
- return true;
1422
- } catch {
1423
- return false;
1424
- }
1425
- }
1426
1504
  function formatBrowserCandidate(candidate) {
1427
1505
  return `${candidate.label} (${candidate.resolvedExecutable})`;
1428
1506
  }
@@ -1480,19 +1558,29 @@ function inspectBrowserVersion(executable, signal) {
1480
1558
  });
1481
1559
  });
1482
1560
  }
1483
- function isPortAvailable(host, port) {
1484
- return new Promise((resolveAvailability, reject) => {
1561
+ async function allocateBrowserPort(host) {
1562
+ const port = await probeBrowserPort(host, 0);
1563
+ if (port === void 0) throw new Error("Cannot allocate an available DevTools port.");
1564
+ return port;
1565
+ }
1566
+ async function isPortAvailable(host, port) {
1567
+ return await probeBrowserPort(host, port) !== void 0;
1568
+ }
1569
+ function probeBrowserPort(host, port) {
1570
+ return new Promise((resolvePort, reject) => {
1485
1571
  const server = createServer();
1486
1572
  const normalizedHost = host.replace(/^\[(.*)]$/, "$1");
1487
1573
  server.unref();
1488
1574
  server.once("error", (error) => {
1489
- if (error.code === "EADDRINUSE") resolveAvailability(false);
1575
+ if (error.code === "EADDRINUSE") resolvePort(void 0);
1490
1576
  else reject(error);
1491
1577
  });
1492
1578
  server.listen(port, normalizedHost, () => {
1579
+ const address = server.address();
1493
1580
  server.close((error) => {
1494
1581
  if (error) reject(error);
1495
- else resolveAvailability(true);
1582
+ else if (!address || typeof address === "string") reject(new Error("DevTools port probe has no address."));
1583
+ else resolvePort(address.port);
1496
1584
  });
1497
1585
  });
1498
1586
  });
@@ -1553,4 +1641,4 @@ export {
1553
1641
  endpointConfigHint,
1554
1642
  formatPageListItem
1555
1643
  };
1556
- //# sourceMappingURL=chunk-TVVYQ5KM.ts.map
1644
+ //# sourceMappingURL=chunk-WTMZID3G.ts.map