opensteer 0.6.11 → 0.6.13

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.
@@ -424,10 +424,7 @@ var import_fs5 = require("fs");
424
424
  var import_crypto = require("crypto");
425
425
 
426
426
  // src/browser/pool.ts
427
- var import_node_child_process = require("child_process");
428
- var import_promises2 = require("fs/promises");
429
- var import_node_net = require("net");
430
- var import_playwright = require("playwright");
427
+ var import_playwright2 = require("playwright");
431
428
 
432
429
  // src/browser/cdp-proxy.ts
433
430
  var import_ws = __toESM(require("ws"), 1);
@@ -920,281 +917,1892 @@ function listLocalChromeProfiles(userDataDir = detectChromePaths().defaultUserDa
920
917
  }
921
918
 
922
919
  // src/browser/persistent-profile.ts
920
+ var import_node_crypto3 = require("crypto");
921
+ var import_node_child_process2 = require("child_process");
922
+ var import_node_fs3 = require("fs");
923
+ var import_promises4 = require("fs/promises");
924
+ var import_node_os = require("os");
925
+ var import_node_path4 = require("path");
926
+ var import_node_util2 = require("util");
927
+
928
+ // src/browser/persistent-profile-coordination.ts
929
+ var import_node_path2 = require("path");
930
+
931
+ // src/browser/dir-lock.ts
923
932
  var import_node_crypto = require("crypto");
924
933
  var import_node_fs = require("fs");
925
- var import_promises = require("fs/promises");
926
- var import_node_os = require("os");
934
+ var import_promises2 = require("fs/promises");
927
935
  var import_node_path = require("path");
928
- var OPENSTEER_META_FILE = ".opensteer-meta.json";
936
+
937
+ // src/browser/process-owner.ts
938
+ var import_node_child_process = require("child_process");
939
+ var import_promises = require("fs/promises");
940
+ var import_node_util = require("util");
941
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
929
942
  var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
930
943
  var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
931
- var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
932
- "SingletonCookie",
933
- "SingletonLock",
934
- "SingletonSocket",
935
- "DevToolsActivePort",
936
- "lockfile"
937
- ]);
938
- var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
939
- ...CHROME_SINGLETON_ENTRIES,
940
- OPENSTEER_META_FILE
941
- ]);
942
- var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
943
- "Crash Reports",
944
- "Crashpad",
945
- "BrowserMetrics",
946
- "GrShaderCache",
947
- "ShaderCache",
948
- "GraphiteDawnCache",
949
- "component_crx_cache",
950
- "Crowd Deny",
951
- "hyphen-data",
952
- "OnDeviceHeadSuggestModel",
953
- "OptimizationGuidePredictionModels",
954
- "Segmentation Platform",
955
- "SmartCardDeviceNames",
956
- "WidevineCdm",
957
- "pnacl"
958
- ]);
959
- async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
960
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
961
- const targetUserDataDir = (0, import_node_path.join)(
962
- expandHome(profilesRootDir),
963
- buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
964
- );
965
- const sourceProfileDir = (0, import_node_path.join)(resolvedSourceUserDataDir, profileDirectory);
966
- const metadata = buildPersistentProfileMetadata(
967
- resolvedSourceUserDataDir,
968
- profileDirectory
969
- );
970
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(targetUserDataDir), { recursive: true });
971
- await cleanOrphanedTempDirs(
972
- (0, import_node_path.dirname)(targetUserDataDir),
973
- (0, import_node_path.basename)(targetUserDataDir)
974
- );
975
- if (!(0, import_node_fs.existsSync)(sourceProfileDir)) {
976
- throw new Error(
977
- `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
978
- );
944
+ var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
945
+ var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
946
+ var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
947
+ var CURRENT_PROCESS_OWNER = {
948
+ pid: process.pid,
949
+ processStartedAtMs: PROCESS_STARTED_AT_MS
950
+ };
951
+ var linuxClockTicksPerSecondPromise = null;
952
+ function parseProcessOwner(value) {
953
+ if (!value || typeof value !== "object") {
954
+ return null;
955
+ }
956
+ const parsed = value;
957
+ const pid = Number(parsed.pid);
958
+ const processStartedAtMs = Number(parsed.processStartedAtMs);
959
+ if (!Number.isInteger(pid) || pid <= 0) {
960
+ return null;
961
+ }
962
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
963
+ return null;
979
964
  }
980
- const created = await createPersistentProfileClone(
981
- resolvedSourceUserDataDir,
982
- sourceProfileDir,
983
- targetUserDataDir,
984
- profileDirectory,
985
- metadata
986
- );
987
- await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
988
965
  return {
989
- created,
990
- userDataDir: targetUserDataDir
966
+ pid,
967
+ processStartedAtMs
991
968
  };
992
969
  }
993
- async function clearPersistentProfileSingletons(userDataDir) {
994
- await Promise.all(
995
- [...CHROME_SINGLETON_ENTRIES].map(
996
- (entry) => (0, import_promises.rm)((0, import_node_path.join)(userDataDir, entry), {
997
- force: true,
998
- recursive: true
999
- }).catch(() => void 0)
1000
- )
1001
- );
970
+ function processOwnersEqual(left, right) {
971
+ if (!left || !right) {
972
+ return left === right;
973
+ }
974
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1002
975
  }
1003
- function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
1004
- const hash = (0, import_node_crypto.createHash)("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
1005
- const sourceLabel = sanitizePathSegment((0, import_node_path.basename)(sourceUserDataDir) || "user-data");
1006
- const profileLabel = sanitizePathSegment(profileDirectory || "Default");
1007
- return `${sourceLabel}-${profileLabel}-${hash}`;
976
+ async function getProcessLiveness(owner) {
977
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(
978
+ owner.processStartedAtMs,
979
+ PROCESS_STARTED_AT_MS
980
+ )) {
981
+ return "live";
982
+ }
983
+ const startedAtMs = await readProcessStartedAtMs(owner.pid);
984
+ if (typeof startedAtMs === "number") {
985
+ return hasMatchingProcessStartTime(
986
+ owner.processStartedAtMs,
987
+ startedAtMs
988
+ ) ? "live" : "dead";
989
+ }
990
+ return isProcessRunning(owner.pid) ? "unknown" : "dead";
1008
991
  }
1009
- function defaultPersistentProfilesRootDir() {
1010
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".opensteer", "real-browser-profiles");
992
+ function isProcessRunning(pid) {
993
+ try {
994
+ process.kill(pid, 0);
995
+ return true;
996
+ } catch (error) {
997
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
998
+ return code !== "ESRCH";
999
+ }
1011
1000
  }
1012
- function sanitizePathSegment(value) {
1013
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
1014
- return sanitized.replace(/^-|-$/g, "") || "profile";
1001
+ async function readProcessOwner(pid) {
1002
+ const processStartedAtMs = await readProcessStartedAtMs(pid);
1003
+ if (processStartedAtMs === null) {
1004
+ return null;
1005
+ }
1006
+ return {
1007
+ pid,
1008
+ processStartedAtMs
1009
+ };
1015
1010
  }
1016
- function isProfileDirectory(userDataDir, entry) {
1017
- return (0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, entry, "Preferences"));
1011
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1012
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
1018
1013
  }
1019
- async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
1020
- let entries;
1014
+ async function readProcessStartedAtMs(pid) {
1015
+ if (pid <= 0) {
1016
+ return null;
1017
+ }
1018
+ if (process.platform === "linux") {
1019
+ return await readLinuxProcessStartedAtMs(pid);
1020
+ }
1021
+ if (process.platform === "win32") {
1022
+ return await readWindowsProcessStartedAtMs(pid);
1023
+ }
1024
+ return await readPsProcessStartedAtMs(pid);
1025
+ }
1026
+ async function readLinuxProcessStartedAtMs(pid) {
1027
+ let statRaw;
1021
1028
  try {
1022
- entries = await (0, import_promises.readdir)(sourceUserDataDir);
1029
+ statRaw = await (0, import_promises.readFile)(`/proc/${pid}/stat`, "utf8");
1023
1030
  } catch {
1024
- return;
1031
+ return null;
1025
1032
  }
1026
- const copyTasks = [];
1027
- for (const entry of entries) {
1028
- if (COPY_SKIP_ENTRIES.has(entry)) continue;
1029
- if (entry === targetProfileDirectory) continue;
1030
- const sourcePath = (0, import_node_path.join)(sourceUserDataDir, entry);
1031
- const targetPath = (0, import_node_path.join)(targetUserDataDir, entry);
1032
- if ((0, import_node_fs.existsSync)(targetPath)) continue;
1033
- let entryStat;
1034
- try {
1035
- entryStat = await (0, import_promises.stat)(sourcePath);
1036
- } catch {
1037
- continue;
1038
- }
1039
- if (entryStat.isFile()) {
1040
- copyTasks.push((0, import_promises.copyFile)(sourcePath, targetPath).catch(() => void 0));
1041
- } else if (entryStat.isDirectory()) {
1042
- if (isProfileDirectory(sourceUserDataDir, entry)) continue;
1043
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
1044
- copyTasks.push(
1045
- (0, import_promises.cp)(sourcePath, targetPath, { recursive: true }).catch(
1046
- () => void 0
1047
- )
1048
- );
1049
- }
1033
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
1034
+ if (startTicks === null) {
1035
+ return null;
1050
1036
  }
1051
- await Promise.all(copyTasks);
1052
- }
1053
- async function writePersistentProfileMetadata(userDataDir, metadata) {
1054
- await (0, import_promises.writeFile)(
1055
- (0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE),
1056
- JSON.stringify(metadata, null, 2)
1037
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1038
+ readLinuxBootTimeMs(),
1039
+ readLinuxClockTicksPerSecond()
1040
+ ]);
1041
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
1042
+ return null;
1043
+ }
1044
+ return Math.floor(
1045
+ bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
1057
1046
  );
1058
1047
  }
1059
- function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1060
- return {
1061
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1062
- profileDirectory,
1063
- source: sourceUserDataDir
1064
- };
1048
+ function parseLinuxProcessStartTicks(statRaw) {
1049
+ const closingParenIndex = statRaw.lastIndexOf(")");
1050
+ if (closingParenIndex === -1) {
1051
+ return null;
1052
+ }
1053
+ const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
1054
+ const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
1055
+ return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
1065
1056
  }
1066
- async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1067
- if ((0, import_node_fs.existsSync)(targetUserDataDir)) {
1068
- return false;
1057
+ async function readLinuxBootTimeMs() {
1058
+ try {
1059
+ const statRaw = await (0, import_promises.readFile)("/proc/stat", "utf8");
1060
+ const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
1061
+ if (!bootTimeLine) {
1062
+ return null;
1063
+ }
1064
+ const bootTimeSeconds = Number.parseInt(
1065
+ bootTimeLine.slice("btime ".length),
1066
+ 10
1067
+ );
1068
+ return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
1069
+ } catch {
1070
+ return null;
1069
1071
  }
1070
- const tempUserDataDir = await (0, import_promises.mkdtemp)(
1071
- buildPersistentProfileTempDirPrefix(targetUserDataDir)
1072
- );
1073
- let published = false;
1072
+ }
1073
+ async function readLinuxClockTicksPerSecond() {
1074
+ if (!linuxClockTicksPerSecondPromise) {
1075
+ linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
1076
+ encoding: "utf8",
1077
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1078
+ }).then(({ stdout }) => {
1079
+ const value = Number.parseInt(stdout.trim(), 10);
1080
+ return Number.isFinite(value) && value > 0 ? value : null;
1081
+ }).catch(() => null);
1082
+ }
1083
+ return await linuxClockTicksPerSecondPromise;
1084
+ }
1085
+ async function readWindowsProcessStartedAtMs(pid) {
1074
1086
  try {
1075
- await (0, import_promises.cp)(sourceProfileDir, (0, import_node_path.join)(tempUserDataDir, profileDirectory), {
1076
- recursive: true
1077
- });
1078
- await copyRootLevelEntries(
1079
- sourceUserDataDir,
1080
- tempUserDataDir,
1081
- profileDirectory
1087
+ const { stdout } = await execFileAsync(
1088
+ "powershell.exe",
1089
+ [
1090
+ "-NoProfile",
1091
+ "-Command",
1092
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString("o")`
1093
+ ],
1094
+ {
1095
+ encoding: "utf8",
1096
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1097
+ }
1082
1098
  );
1083
- await writePersistentProfileMetadata(tempUserDataDir, metadata);
1084
- try {
1085
- await (0, import_promises.rename)(tempUserDataDir, targetUserDataDir);
1086
- } catch (error) {
1087
- if (wasProfilePublishedByAnotherProcess(error, targetUserDataDir)) {
1088
- return false;
1099
+ const isoTimestamp = stdout.trim();
1100
+ if (!isoTimestamp) {
1101
+ return null;
1102
+ }
1103
+ const startedAtMs = Date.parse(isoTimestamp);
1104
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1105
+ } catch {
1106
+ return null;
1107
+ }
1108
+ }
1109
+ async function readPsProcessStartedAtMs(pid) {
1110
+ try {
1111
+ const { stdout } = await execFileAsync(
1112
+ "ps",
1113
+ ["-o", "lstart=", "-p", String(pid)],
1114
+ {
1115
+ encoding: "utf8",
1116
+ env: PS_COMMAND_ENV,
1117
+ maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
1089
1118
  }
1090
- throw error;
1119
+ );
1120
+ const startedAt = stdout.trim();
1121
+ if (!startedAt) {
1122
+ return null;
1091
1123
  }
1092
- published = true;
1093
- return true;
1124
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
1125
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1126
+ } catch {
1127
+ return null;
1128
+ }
1129
+ }
1130
+
1131
+ // src/browser/dir-lock.ts
1132
+ var LOCK_OWNER_FILE = "owner.json";
1133
+ var LOCK_RECLAIMER_DIR = "reclaimer";
1134
+ var LOCK_RETRY_DELAY_MS = 50;
1135
+ async function withDirLock(lockDirPath, action) {
1136
+ const releaseLock = await acquireDirLock(lockDirPath);
1137
+ try {
1138
+ return await action();
1094
1139
  } finally {
1095
- if (!published) {
1096
- await (0, import_promises.rm)(tempUserDataDir, {
1140
+ await releaseLock();
1141
+ }
1142
+ }
1143
+ async function acquireDirLock(lockDirPath) {
1144
+ while (true) {
1145
+ const releaseLock = await tryAcquireDirLock(lockDirPath);
1146
+ if (releaseLock) {
1147
+ return releaseLock;
1148
+ }
1149
+ await sleep(LOCK_RETRY_DELAY_MS);
1150
+ }
1151
+ }
1152
+ async function tryAcquireDirLock(lockDirPath) {
1153
+ await (0, import_promises2.mkdir)((0, import_node_path.dirname)(lockDirPath), { recursive: true });
1154
+ while (true) {
1155
+ const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1156
+ try {
1157
+ await (0, import_promises2.mkdir)(tempLockDirPath);
1158
+ await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
1159
+ try {
1160
+ await (0, import_promises2.rename)(tempLockDirPath, lockDirPath);
1161
+ break;
1162
+ } catch (error) {
1163
+ if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
1164
+ throw error;
1165
+ }
1166
+ }
1167
+ } finally {
1168
+ await (0, import_promises2.rm)(tempLockDirPath, {
1097
1169
  recursive: true,
1098
1170
  force: true
1099
1171
  }).catch(() => void 0);
1100
1172
  }
1173
+ const owner = await readLockOwner(lockDirPath);
1174
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1175
+ continue;
1176
+ }
1177
+ return null;
1101
1178
  }
1179
+ return async () => {
1180
+ await (0, import_promises2.rm)(lockDirPath, {
1181
+ recursive: true,
1182
+ force: true
1183
+ }).catch(() => void 0);
1184
+ };
1102
1185
  }
1103
- async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1104
- if ((0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE))) {
1105
- return;
1186
+ async function isDirLockHeld(lockDirPath) {
1187
+ if (!(0, import_node_fs.existsSync)(lockDirPath)) {
1188
+ return false;
1106
1189
  }
1107
- await writePersistentProfileMetadata(userDataDir, metadata);
1190
+ const owner = await readLockOwner(lockDirPath);
1191
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1192
+ return false;
1193
+ }
1194
+ return (0, import_node_fs.existsSync)(lockDirPath);
1108
1195
  }
1109
- function wasProfilePublishedByAnotherProcess(error, targetUserDataDir) {
1110
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1111
- return (0, import_node_fs.existsSync)(targetUserDataDir) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1196
+ function getErrorCode(error) {
1197
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1112
1198
  }
1113
- function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1114
- return (0, import_node_path.join)(
1115
- (0, import_node_path.dirname)(targetUserDataDir),
1116
- `${(0, import_node_path.basename)(targetUserDataDir)}-tmp-${process.pid}-${PROCESS_STARTED_AT_MS}-`
1199
+ function wasDirPublishedByAnotherProcess(error, targetDirPath) {
1200
+ const code = getErrorCode(error);
1201
+ return (0, import_node_fs.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1202
+ }
1203
+ async function writeLockOwner(lockDirPath, owner) {
1204
+ await (0, import_promises2.writeFile)((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
1205
+ }
1206
+ async function readLockOwner(lockDirPath) {
1207
+ return await readLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE));
1208
+ }
1209
+ async function readLockParticipant(filePath) {
1210
+ return (await readLockParticipantRecord(filePath)).owner;
1211
+ }
1212
+ async function readLockParticipantRecord(filePath) {
1213
+ try {
1214
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
1215
+ const owner = parseProcessOwner(JSON.parse(raw));
1216
+ return {
1217
+ exists: true,
1218
+ owner
1219
+ };
1220
+ } catch (error) {
1221
+ return {
1222
+ exists: getErrorCode(error) !== "ENOENT",
1223
+ owner: null
1224
+ };
1225
+ }
1226
+ }
1227
+ async function readLockReclaimerRecord(lockDirPath) {
1228
+ return await readLockParticipantRecord(
1229
+ (0, import_node_path.join)(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
1117
1230
  );
1118
1231
  }
1119
- async function cleanOrphanedTempDirs(profilesDir, targetBaseName) {
1120
- let entries;
1232
+ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1233
+ if (!await tryAcquireLockReclaimer(lockDirPath)) {
1234
+ return false;
1235
+ }
1236
+ let reclaimed = false;
1121
1237
  try {
1122
- entries = await (0, import_promises.readdir)(profilesDir, {
1123
- encoding: "utf8",
1124
- withFileTypes: true
1125
- });
1126
- } catch {
1127
- return;
1238
+ const owner = await readLockOwner(lockDirPath);
1239
+ if (!processOwnersEqual(owner, expectedOwner)) {
1240
+ return false;
1241
+ }
1242
+ if (owner && await getProcessLiveness(owner) !== "dead") {
1243
+ return false;
1244
+ }
1245
+ await (0, import_promises2.rm)(lockDirPath, {
1246
+ recursive: true,
1247
+ force: true
1248
+ }).catch(() => void 0);
1249
+ reclaimed = !(0, import_node_fs.existsSync)(lockDirPath);
1250
+ return reclaimed;
1251
+ } finally {
1252
+ if (!reclaimed) {
1253
+ await (0, import_promises2.rm)(buildLockReclaimerDirPath(lockDirPath), {
1254
+ recursive: true,
1255
+ force: true
1256
+ }).catch(() => void 0);
1257
+ }
1128
1258
  }
1129
- const tempDirPrefix = `${targetBaseName}-tmp-`;
1130
- await Promise.all(
1131
- entries.map(async (entry) => {
1132
- if (!entry.isDirectory() || !entry.name.startsWith(tempDirPrefix)) {
1133
- return;
1134
- }
1135
- if (isTempDirOwnedByLiveProcess(entry.name, tempDirPrefix)) {
1136
- return;
1259
+ }
1260
+ async function tryAcquireLockReclaimer(lockDirPath) {
1261
+ const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
1262
+ while (true) {
1263
+ const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1264
+ try {
1265
+ await (0, import_promises2.mkdir)(tempReclaimerDirPath);
1266
+ await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
1267
+ try {
1268
+ await (0, import_promises2.rename)(tempReclaimerDirPath, reclaimerDirPath);
1269
+ return true;
1270
+ } catch (error) {
1271
+ if (getErrorCode(error) === "ENOENT") {
1272
+ return false;
1273
+ }
1274
+ if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
1275
+ throw error;
1276
+ }
1277
+ }
1278
+ } catch (error) {
1279
+ const code = getErrorCode(error);
1280
+ if (code === "ENOENT") {
1281
+ return false;
1282
+ }
1283
+ throw error;
1284
+ } finally {
1285
+ await (0, import_promises2.rm)(tempReclaimerDirPath, {
1286
+ recursive: true,
1287
+ force: true
1288
+ }).catch(() => void 0);
1289
+ }
1290
+ const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
1291
+ if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
1292
+ return false;
1293
+ }
1294
+ if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
1295
+ return false;
1296
+ }
1297
+ await (0, import_promises2.rm)(reclaimerDirPath, {
1298
+ recursive: true,
1299
+ force: true
1300
+ }).catch(() => void 0);
1301
+ }
1302
+ }
1303
+ function buildLockReclaimerDirPath(lockDirPath) {
1304
+ return (0, import_node_path.join)(lockDirPath, LOCK_RECLAIMER_DIR);
1305
+ }
1306
+ async function sleep(ms) {
1307
+ await new Promise((resolve) => setTimeout(resolve, ms));
1308
+ }
1309
+
1310
+ // src/browser/persistent-profile-coordination.ts
1311
+ var PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS = 50;
1312
+ async function withPersistentProfileControlLock(targetUserDataDir, action) {
1313
+ return await withDirLock(
1314
+ buildPersistentProfileControlLockDirPath(targetUserDataDir),
1315
+ action
1316
+ );
1317
+ }
1318
+ async function acquirePersistentProfileWriteLock(targetUserDataDir) {
1319
+ const controlLockDirPath = buildPersistentProfileControlLockDirPath(targetUserDataDir);
1320
+ const writeLockDirPath = buildPersistentProfileWriteLockDirPath(
1321
+ targetUserDataDir
1322
+ );
1323
+ while (true) {
1324
+ let releaseWriteLock = null;
1325
+ const releaseControlLock = await acquireDirLock(controlLockDirPath);
1326
+ try {
1327
+ releaseWriteLock = await tryAcquireDirLock(writeLockDirPath);
1328
+ } finally {
1329
+ await releaseControlLock();
1330
+ }
1331
+ if (releaseWriteLock) {
1332
+ return releaseWriteLock;
1333
+ }
1334
+ await sleep2(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
1335
+ }
1336
+ }
1337
+ async function isPersistentProfileWriteLocked(targetUserDataDir) {
1338
+ return await isDirLockHeld(
1339
+ buildPersistentProfileWriteLockDirPath(targetUserDataDir)
1340
+ );
1341
+ }
1342
+ function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
1343
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(targetUserDataDir), `${(0, import_node_path2.basename)(targetUserDataDir)}.lock`);
1344
+ }
1345
+ function buildPersistentProfileControlLockDirPath(targetUserDataDir) {
1346
+ return (0, import_node_path2.join)(
1347
+ (0, import_node_path2.dirname)(targetUserDataDir),
1348
+ `${(0, import_node_path2.basename)(targetUserDataDir)}.control.lock`
1349
+ );
1350
+ }
1351
+ async function sleep2(ms) {
1352
+ await new Promise((resolve) => setTimeout(resolve, ms));
1353
+ }
1354
+
1355
+ // src/browser/shared-real-browser-session-state.ts
1356
+ var import_node_crypto2 = require("crypto");
1357
+ var import_node_fs2 = require("fs");
1358
+ var import_promises3 = require("fs/promises");
1359
+ var import_node_path3 = require("path");
1360
+ var SHARED_SESSION_METADATA_FILE = "session.json";
1361
+ var SHARED_SESSION_CLIENTS_DIR = "clients";
1362
+ var SHARED_SESSION_RETRY_DELAY_MS = 50;
1363
+ var SHARED_SESSION_METADATA_TEMP_FILE_PREFIX = `${SHARED_SESSION_METADATA_FILE}.`;
1364
+ var SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX = ".tmp";
1365
+ function buildSharedSessionDirPath(persistentUserDataDir) {
1366
+ return (0, import_node_path3.join)(
1367
+ (0, import_node_path3.dirname)(persistentUserDataDir),
1368
+ `${(0, import_node_path3.basename)(persistentUserDataDir)}.session`
1369
+ );
1370
+ }
1371
+ function buildSharedSessionLockPath(persistentUserDataDir) {
1372
+ return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
1373
+ }
1374
+ function buildSharedSessionClientsDirPath(persistentUserDataDir) {
1375
+ return (0, import_node_path3.join)(
1376
+ buildSharedSessionDirPath(persistentUserDataDir),
1377
+ SHARED_SESSION_CLIENTS_DIR
1378
+ );
1379
+ }
1380
+ function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
1381
+ return (0, import_node_path3.join)(
1382
+ buildSharedSessionClientsDirPath(persistentUserDataDir),
1383
+ `${clientId}.json`
1384
+ );
1385
+ }
1386
+ async function readSharedSessionMetadata(persistentUserDataDir) {
1387
+ return (await readSharedSessionMetadataRecord(persistentUserDataDir)).metadata;
1388
+ }
1389
+ async function writeSharedSessionMetadata(persistentUserDataDir, metadata) {
1390
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
1391
+ const metadataPath = buildSharedSessionMetadataPath(persistentUserDataDir);
1392
+ const tempPath = buildSharedSessionMetadataTempPath(sessionDirPath);
1393
+ await (0, import_promises3.mkdir)(sessionDirPath, { recursive: true });
1394
+ try {
1395
+ await (0, import_promises3.writeFile)(tempPath, JSON.stringify(metadata, null, 2));
1396
+ await (0, import_promises3.rename)(tempPath, metadataPath);
1397
+ } finally {
1398
+ await (0, import_promises3.rm)(tempPath, { force: true }).catch(() => void 0);
1399
+ }
1400
+ }
1401
+ async function hasLiveSharedRealBrowserSession(persistentUserDataDir) {
1402
+ const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
1403
+ const metadataRecord = await readSharedSessionMetadataRecord(
1404
+ persistentUserDataDir
1405
+ );
1406
+ if (!metadataRecord.exists) {
1407
+ return await hasLiveSharedSessionPublisherOrClients(sessionDirPath);
1408
+ }
1409
+ if (!metadataRecord.metadata) {
1410
+ return true;
1411
+ }
1412
+ if (await getProcessLiveness(metadataRecord.metadata.browserOwner) === "dead") {
1413
+ await (0, import_promises3.rm)(sessionDirPath, {
1414
+ force: true,
1415
+ recursive: true
1416
+ }).catch(() => void 0);
1417
+ return false;
1418
+ }
1419
+ return true;
1420
+ }
1421
+ async function waitForSharedRealBrowserSessionToDrain(persistentUserDataDir) {
1422
+ while (true) {
1423
+ if (!await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
1424
+ return;
1425
+ }
1426
+ await sleep3(SHARED_SESSION_RETRY_DELAY_MS);
1427
+ }
1428
+ }
1429
+ async function readSharedSessionMetadataRecord(persistentUserDataDir) {
1430
+ try {
1431
+ const raw = await (0, import_promises3.readFile)(
1432
+ buildSharedSessionMetadataPath(persistentUserDataDir),
1433
+ "utf8"
1434
+ );
1435
+ return {
1436
+ exists: true,
1437
+ metadata: parseSharedSessionMetadata(JSON.parse(raw))
1438
+ };
1439
+ } catch (error) {
1440
+ return {
1441
+ exists: getErrorCode2(error) !== "ENOENT",
1442
+ metadata: null
1443
+ };
1444
+ }
1445
+ }
1446
+ async function hasLiveSharedSessionPublisherOrClients(sessionDirPath) {
1447
+ if (!(0, import_node_fs2.existsSync)(sessionDirPath)) {
1448
+ return false;
1449
+ }
1450
+ let entries;
1451
+ try {
1452
+ entries = await readDirNames(sessionDirPath);
1453
+ } catch (error) {
1454
+ return getErrorCode2(error) !== "ENOENT";
1455
+ }
1456
+ let hasUnknownEntries = false;
1457
+ for (const entry of entries) {
1458
+ if (entry === SHARED_SESSION_METADATA_FILE) {
1459
+ return true;
1460
+ }
1461
+ if (entry === SHARED_SESSION_CLIENTS_DIR) {
1462
+ if (await hasDirectoryEntries((0, import_node_path3.join)(sessionDirPath, entry))) {
1463
+ return true;
1464
+ }
1465
+ continue;
1466
+ }
1467
+ const owner = parseSharedSessionMetadataTempOwner(entry);
1468
+ if (!owner) {
1469
+ if (isSharedSessionMetadataTempFile(entry)) {
1470
+ continue;
1471
+ }
1472
+ hasUnknownEntries = true;
1473
+ continue;
1474
+ }
1475
+ if (await getProcessLiveness(owner) !== "dead") {
1476
+ return true;
1477
+ }
1478
+ }
1479
+ if (hasUnknownEntries) {
1480
+ return true;
1481
+ }
1482
+ await (0, import_promises3.rm)(sessionDirPath, {
1483
+ force: true,
1484
+ recursive: true
1485
+ }).catch(() => void 0);
1486
+ return false;
1487
+ }
1488
+ function buildSharedSessionMetadataPath(persistentUserDataDir) {
1489
+ return (0, import_node_path3.join)(
1490
+ buildSharedSessionDirPath(persistentUserDataDir),
1491
+ SHARED_SESSION_METADATA_FILE
1492
+ );
1493
+ }
1494
+ function buildSharedSessionMetadataTempPath(sessionDirPath) {
1495
+ return (0, import_node_path3.join)(
1496
+ sessionDirPath,
1497
+ [
1498
+ SHARED_SESSION_METADATA_FILE,
1499
+ CURRENT_PROCESS_OWNER.pid,
1500
+ CURRENT_PROCESS_OWNER.processStartedAtMs,
1501
+ (0, import_node_crypto2.randomUUID)(),
1502
+ "tmp"
1503
+ ].join(".")
1504
+ );
1505
+ }
1506
+ function parseSharedSessionMetadata(value) {
1507
+ if (!value || typeof value !== "object") {
1508
+ return null;
1509
+ }
1510
+ const parsed = value;
1511
+ const browserOwner = parseProcessOwner(parsed.browserOwner);
1512
+ const stateOwner = parseProcessOwner(parsed.stateOwner);
1513
+ const state = parsed.state === "launching" || parsed.state === "ready" || parsed.state === "closing" ? parsed.state : null;
1514
+ if (!browserOwner || !stateOwner || typeof parsed.createdAt !== "string" || typeof parsed.debugPort !== "number" || typeof parsed.executablePath !== "string" || typeof parsed.headless !== "boolean" || typeof parsed.persistentUserDataDir !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.sessionId !== "string" || !state) {
1515
+ return null;
1516
+ }
1517
+ return {
1518
+ browserOwner,
1519
+ createdAt: parsed.createdAt,
1520
+ debugPort: parsed.debugPort,
1521
+ executablePath: parsed.executablePath,
1522
+ headless: parsed.headless,
1523
+ persistentUserDataDir: parsed.persistentUserDataDir,
1524
+ profileDirectory: parsed.profileDirectory,
1525
+ sessionId: parsed.sessionId,
1526
+ state,
1527
+ stateOwner
1528
+ };
1529
+ }
1530
+ function parseSharedSessionMetadataTempOwner(entryName) {
1531
+ if (!isSharedSessionMetadataTempFile(entryName)) {
1532
+ return null;
1533
+ }
1534
+ const segments = entryName.split(".");
1535
+ if (segments.length < 5) {
1536
+ return null;
1537
+ }
1538
+ return parseProcessOwner({
1539
+ pid: Number.parseInt(segments[2] ?? "", 10),
1540
+ processStartedAtMs: Number.parseInt(segments[3] ?? "", 10)
1541
+ });
1542
+ }
1543
+ function isSharedSessionMetadataTempFile(entryName) {
1544
+ return entryName.startsWith(SHARED_SESSION_METADATA_TEMP_FILE_PREFIX) && entryName.endsWith(SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX);
1545
+ }
1546
+ function getErrorCode2(error) {
1547
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1548
+ }
1549
+ async function hasDirectoryEntries(dirPath) {
1550
+ try {
1551
+ return (await readDirNames(dirPath)).length > 0;
1552
+ } catch (error) {
1553
+ return getErrorCode2(error) !== "ENOENT";
1554
+ }
1555
+ }
1556
+ async function readDirNames(dirPath) {
1557
+ return await (0, import_promises3.readdir)(dirPath, { encoding: "utf8" });
1558
+ }
1559
+ async function sleep3(ms) {
1560
+ await new Promise((resolve) => setTimeout(resolve, ms));
1561
+ }
1562
+
1563
+ // src/browser/persistent-profile.ts
1564
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
1565
+ var OPENSTEER_META_FILE = ".opensteer-meta.json";
1566
+ var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
1567
+ var OPENSTEER_RUNTIME_CREATING_FILE = ".opensteer-runtime-creating.json";
1568
+ var PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
1569
+ var PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
1570
+ var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
1571
+ "SingletonCookie",
1572
+ "SingletonLock",
1573
+ "SingletonSocket",
1574
+ "DevToolsActivePort",
1575
+ "lockfile"
1576
+ ]);
1577
+ var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
1578
+ ...CHROME_SINGLETON_ENTRIES,
1579
+ OPENSTEER_META_FILE,
1580
+ OPENSTEER_RUNTIME_META_FILE,
1581
+ OPENSTEER_RUNTIME_CREATING_FILE
1582
+ ]);
1583
+ var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
1584
+ "Crash Reports",
1585
+ "Crashpad",
1586
+ "BrowserMetrics",
1587
+ "GrShaderCache",
1588
+ "ShaderCache",
1589
+ "GraphiteDawnCache",
1590
+ "component_crx_cache",
1591
+ "Crowd Deny",
1592
+ "hyphen-data",
1593
+ "OnDeviceHeadSuggestModel",
1594
+ "OptimizationGuidePredictionModels",
1595
+ "Segmentation Platform",
1596
+ "SmartCardDeviceNames",
1597
+ "WidevineCdm",
1598
+ "pnacl"
1599
+ ]);
1600
+ async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
1601
+ const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1602
+ const targetUserDataDir = (0, import_node_path4.join)(
1603
+ expandHome(profilesRootDir),
1604
+ buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
1605
+ );
1606
+ const sourceProfileDir = (0, import_node_path4.join)(resolvedSourceUserDataDir, profileDirectory);
1607
+ const metadata = buildPersistentProfileMetadata(
1608
+ resolvedSourceUserDataDir,
1609
+ profileDirectory
1610
+ );
1611
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(targetUserDataDir), { recursive: true });
1612
+ if (await isHealthyPersistentProfile(
1613
+ targetUserDataDir,
1614
+ resolvedSourceUserDataDir,
1615
+ profileDirectory
1616
+ ) && !await isPersistentProfileWriteLocked(targetUserDataDir)) {
1617
+ return {
1618
+ created: false,
1619
+ userDataDir: targetUserDataDir
1620
+ };
1621
+ }
1622
+ return await withPersistentProfileWriteAccess(targetUserDataDir, async () => {
1623
+ await recoverPersistentProfileBackup(targetUserDataDir);
1624
+ await cleanOrphanedOwnedDirs(
1625
+ (0, import_node_path4.dirname)(targetUserDataDir),
1626
+ buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
1627
+ );
1628
+ if (!(0, import_node_fs3.existsSync)(sourceProfileDir)) {
1629
+ throw new Error(
1630
+ `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
1631
+ );
1632
+ }
1633
+ const created = await createPersistentProfileClone(
1634
+ resolvedSourceUserDataDir,
1635
+ sourceProfileDir,
1636
+ targetUserDataDir,
1637
+ profileDirectory,
1638
+ metadata
1639
+ );
1640
+ await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
1641
+ return {
1642
+ created,
1643
+ userDataDir: targetUserDataDir
1644
+ };
1645
+ });
1646
+ }
1647
+ async function clearPersistentProfileSingletons(userDataDir) {
1648
+ await Promise.all(
1649
+ [...CHROME_SINGLETON_ENTRIES].map(
1650
+ (entry) => (0, import_promises4.rm)((0, import_node_path4.join)(userDataDir, entry), {
1651
+ force: true,
1652
+ recursive: true
1653
+ }).catch(() => void 0)
1654
+ )
1655
+ );
1656
+ }
1657
+ function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
1658
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
1659
+ const sourceLabel = sanitizePathSegment((0, import_node_path4.basename)(sourceUserDataDir) || "user-data");
1660
+ const profileLabel = sanitizePathSegment(profileDirectory || "Default");
1661
+ return `${sourceLabel}-${profileLabel}-${hash}`;
1662
+ }
1663
+ function defaultPersistentProfilesRootDir() {
1664
+ return (0, import_node_path4.join)((0, import_node_os.homedir)(), ".opensteer", "real-browser-profiles");
1665
+ }
1666
+ function sanitizePathSegment(value) {
1667
+ const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
1668
+ return sanitized.replace(/^-|-$/g, "") || "profile";
1669
+ }
1670
+ function isProfileDirectory(userDataDir, entry) {
1671
+ return (0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, entry, "Preferences"));
1672
+ }
1673
+ async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
1674
+ let entries;
1675
+ try {
1676
+ entries = await (0, import_promises4.readdir)(sourceUserDataDir);
1677
+ } catch {
1678
+ return;
1679
+ }
1680
+ const copyTasks = [];
1681
+ for (const entry of entries) {
1682
+ if (COPY_SKIP_ENTRIES.has(entry)) continue;
1683
+ if (entry === targetProfileDirectory) continue;
1684
+ const sourcePath = (0, import_node_path4.join)(sourceUserDataDir, entry);
1685
+ const targetPath = (0, import_node_path4.join)(targetUserDataDir, entry);
1686
+ if ((0, import_node_fs3.existsSync)(targetPath)) continue;
1687
+ let entryStat;
1688
+ try {
1689
+ entryStat = await (0, import_promises4.stat)(sourcePath);
1690
+ } catch {
1691
+ continue;
1692
+ }
1693
+ if (entryStat.isFile()) {
1694
+ copyTasks.push((0, import_promises4.copyFile)(sourcePath, targetPath).catch(() => void 0));
1695
+ } else if (entryStat.isDirectory()) {
1696
+ if (isProfileDirectory(sourceUserDataDir, entry)) continue;
1697
+ if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
1698
+ copyTasks.push(
1699
+ (0, import_promises4.cp)(sourcePath, targetPath, { recursive: true }).catch(
1700
+ () => void 0
1701
+ )
1702
+ );
1703
+ }
1704
+ }
1705
+ await Promise.all(copyTasks);
1706
+ }
1707
+ async function writePersistentProfileMetadata(userDataDir, metadata) {
1708
+ await (0, import_promises4.writeFile)(
1709
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE),
1710
+ JSON.stringify(metadata, null, 2)
1711
+ );
1712
+ }
1713
+ function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1714
+ return {
1715
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1716
+ profileDirectory,
1717
+ source: sourceUserDataDir
1718
+ };
1719
+ }
1720
+ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1721
+ if ((0, import_node_fs3.existsSync)(targetUserDataDir)) {
1722
+ return false;
1723
+ }
1724
+ const tempUserDataDir = await (0, import_promises4.mkdtemp)(
1725
+ buildPersistentProfileTempDirPrefix(targetUserDataDir)
1726
+ );
1727
+ let published = false;
1728
+ try {
1729
+ await materializePersistentProfileSnapshot(
1730
+ sourceUserDataDir,
1731
+ sourceProfileDir,
1732
+ tempUserDataDir,
1733
+ profileDirectory,
1734
+ metadata
1735
+ );
1736
+ try {
1737
+ await (0, import_promises4.rename)(tempUserDataDir, targetUserDataDir);
1738
+ } catch (error) {
1739
+ if (wasDirPublishedByAnotherProcess2(error, targetUserDataDir)) {
1740
+ return false;
1741
+ }
1742
+ throw error;
1743
+ }
1744
+ published = true;
1745
+ return true;
1746
+ } finally {
1747
+ if (!published) {
1748
+ await (0, import_promises4.rm)(tempUserDataDir, {
1749
+ recursive: true,
1750
+ force: true
1751
+ }).catch(() => void 0);
1752
+ }
1753
+ }
1754
+ }
1755
+ async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1756
+ if (!(0, import_node_fs3.existsSync)(sourceProfileDir)) {
1757
+ throw new Error(
1758
+ `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
1759
+ );
1760
+ }
1761
+ await (0, import_promises4.cp)(sourceProfileDir, (0, import_node_path4.join)(targetUserDataDir, profileDirectory), {
1762
+ recursive: true
1763
+ });
1764
+ await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
1765
+ await writePersistentProfileMetadata(targetUserDataDir, metadata);
1766
+ }
1767
+ async function readRuntimeProfileCreationMarker(userDataDir) {
1768
+ try {
1769
+ const raw = await (0, import_promises4.readFile)(
1770
+ (0, import_node_path4.join)(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
1771
+ "utf8"
1772
+ );
1773
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ }
1778
+ async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1779
+ if ((0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE))) {
1780
+ return;
1781
+ }
1782
+ await writePersistentProfileMetadata(userDataDir, metadata);
1783
+ }
1784
+ async function recoverPersistentProfileBackup(targetUserDataDir) {
1785
+ const backupDirPaths = await listPersistentProfileBackupDirs(targetUserDataDir);
1786
+ if (backupDirPaths.length === 0) {
1787
+ return;
1788
+ }
1789
+ if (!(0, import_node_fs3.existsSync)(targetUserDataDir)) {
1790
+ const [latestBackupDirPath, ...staleBackupDirPaths] = backupDirPaths;
1791
+ await (0, import_promises4.rename)(latestBackupDirPath, targetUserDataDir);
1792
+ await Promise.all(
1793
+ staleBackupDirPaths.map(
1794
+ (backupDirPath) => (0, import_promises4.rm)(backupDirPath, {
1795
+ recursive: true,
1796
+ force: true
1797
+ }).catch(() => void 0)
1798
+ )
1799
+ );
1800
+ return;
1801
+ }
1802
+ await Promise.all(
1803
+ backupDirPaths.map(
1804
+ (backupDirPath) => (0, import_promises4.rm)(backupDirPath, {
1805
+ recursive: true,
1806
+ force: true
1807
+ }).catch(() => void 0)
1808
+ )
1809
+ );
1810
+ }
1811
+ async function listPersistentProfileBackupDirs(targetUserDataDir) {
1812
+ const profilesDir = (0, import_node_path4.dirname)(targetUserDataDir);
1813
+ let entries;
1814
+ try {
1815
+ entries = await (0, import_promises4.readdir)(profilesDir, {
1816
+ encoding: "utf8",
1817
+ withFileTypes: true
1818
+ });
1819
+ } catch {
1820
+ return [];
1821
+ }
1822
+ const backupDirNamePrefix = buildPersistentProfileBackupDirNamePrefix(targetUserDataDir);
1823
+ return entries.filter(
1824
+ (entry) => entry.isDirectory() && entry.name.startsWith(backupDirNamePrefix)
1825
+ ).map((entry) => (0, import_node_path4.join)(profilesDir, entry.name)).sort((leftPath, rightPath) => rightPath.localeCompare(leftPath));
1826
+ }
1827
+ async function readPersistentProfileMetadata(userDataDir) {
1828
+ try {
1829
+ const raw = await (0, import_promises4.readFile)((0, import_node_path4.join)(userDataDir, OPENSTEER_META_FILE), "utf8");
1830
+ const parsed = JSON.parse(raw);
1831
+ if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
1832
+ return null;
1833
+ }
1834
+ return {
1835
+ createdAt: parsed.createdAt,
1836
+ profileDirectory: parsed.profileDirectory,
1837
+ source: parsed.source
1838
+ };
1839
+ } catch {
1840
+ return null;
1841
+ }
1842
+ }
1843
+ async function isHealthyPersistentProfile(userDataDir, expectedSourceUserDataDir, expectedProfileDirectory) {
1844
+ if (!(0, import_node_fs3.existsSync)(userDataDir) || !(0, import_node_fs3.existsSync)((0, import_node_path4.join)(userDataDir, expectedProfileDirectory))) {
1845
+ return false;
1846
+ }
1847
+ const metadata = await readPersistentProfileMetadata(userDataDir);
1848
+ return metadata?.source === expectedSourceUserDataDir && metadata.profileDirectory === expectedProfileDirectory;
1849
+ }
1850
+ function wasDirPublishedByAnotherProcess2(error, targetDirPath) {
1851
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1852
+ return (0, import_node_fs3.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1853
+ }
1854
+ async function withPersistentProfileWriteAccess(targetUserDataDir, action) {
1855
+ const releaseWriteLock = await acquirePersistentProfileWriteLock(
1856
+ targetUserDataDir
1857
+ );
1858
+ try {
1859
+ await waitForRuntimeProfileCreationsToDrain(targetUserDataDir);
1860
+ await waitForSharedRealBrowserSessionToDrain(targetUserDataDir);
1861
+ return await action();
1862
+ } finally {
1863
+ await releaseWriteLock();
1864
+ }
1865
+ }
1866
+ function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1867
+ return (0, import_node_path4.join)(
1868
+ (0, import_node_path4.dirname)(targetUserDataDir),
1869
+ `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-`
1870
+ );
1871
+ }
1872
+ function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
1873
+ return `${(0, import_node_path4.basename)(targetUserDataDir)}-tmp-`;
1874
+ }
1875
+ function buildPersistentProfileBackupDirNamePrefix(targetUserDataDir) {
1876
+ return `${(0, import_node_path4.basename)(targetUserDataDir)}-backup-`;
1877
+ }
1878
+ function buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir) {
1879
+ return (0, import_node_path4.join)(
1880
+ (0, import_node_path4.dirname)(persistentUserDataDir),
1881
+ `${(0, import_node_path4.basename)(persistentUserDataDir)}.creating`
1882
+ );
1883
+ }
1884
+ function buildRuntimeProfileCreationRegistrationPath(persistentUserDataDir, runtimeUserDataDir) {
1885
+ const key = (0, import_node_crypto3.createHash)("sha256").update(runtimeUserDataDir).digest("hex").slice(0, 16);
1886
+ return (0, import_node_path4.join)(
1887
+ buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir),
1888
+ `${key}.json`
1889
+ );
1890
+ }
1891
+ async function clearRuntimeProfileCreationState(runtimeUserDataDir, persistentUserDataDir) {
1892
+ await Promise.all([
1893
+ (0, import_promises4.rm)((0, import_node_path4.join)(runtimeUserDataDir, OPENSTEER_RUNTIME_CREATING_FILE), {
1894
+ force: true
1895
+ }).catch(() => void 0),
1896
+ (0, import_promises4.rm)(
1897
+ buildRuntimeProfileCreationRegistrationPath(
1898
+ persistentUserDataDir,
1899
+ runtimeUserDataDir
1900
+ ),
1901
+ {
1902
+ force: true
1903
+ }
1904
+ ).catch(() => void 0)
1905
+ ]);
1906
+ }
1907
+ async function listRuntimeProfileCreationRegistrations(persistentUserDataDir) {
1908
+ const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
1909
+ persistentUserDataDir
1910
+ );
1911
+ let entries;
1912
+ try {
1913
+ entries = await (0, import_promises4.readdir)(registryDirPath, {
1914
+ encoding: "utf8",
1915
+ withFileTypes: true
1916
+ });
1917
+ } catch {
1918
+ return [];
1919
+ }
1920
+ return await Promise.all(
1921
+ entries.filter((entry) => entry.isFile()).map(async (entry) => {
1922
+ const filePath = (0, import_node_path4.join)(registryDirPath, entry.name);
1923
+ return {
1924
+ filePath,
1925
+ marker: await readRuntimeProfileCreationRegistration(filePath)
1926
+ };
1927
+ })
1928
+ );
1929
+ }
1930
+ async function readRuntimeProfileCreationRegistration(filePath) {
1931
+ try {
1932
+ const raw = await (0, import_promises4.readFile)(filePath, "utf8");
1933
+ return parseRuntimeProfileCreationMarker(JSON.parse(raw));
1934
+ } catch {
1935
+ return null;
1936
+ }
1937
+ }
1938
+ async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
1939
+ let entries;
1940
+ try {
1941
+ entries = await (0, import_promises4.readdir)(rootDir, {
1942
+ encoding: "utf8",
1943
+ withFileTypes: true
1944
+ });
1945
+ } catch {
1946
+ return;
1947
+ }
1948
+ await Promise.all(
1949
+ entries.map(async (entry) => {
1950
+ if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
1951
+ return;
1952
+ }
1953
+ if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
1954
+ return;
1955
+ }
1956
+ await (0, import_promises4.rm)((0, import_node_path4.join)(rootDir, entry.name), {
1957
+ recursive: true,
1958
+ force: true
1959
+ }).catch(() => void 0);
1960
+ })
1961
+ );
1962
+ }
1963
+ async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
1964
+ const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
1965
+ return owner ? await getProcessLiveness(owner) !== "dead" : false;
1966
+ }
1967
+ async function hasActiveRuntimeProfileCreations(persistentUserDataDir) {
1968
+ const registrations = await listRuntimeProfileCreationRegistrations(
1969
+ persistentUserDataDir
1970
+ );
1971
+ let hasLiveCreation = false;
1972
+ for (const registration of registrations) {
1973
+ const marker = registration.marker;
1974
+ if (!marker || marker.persistentUserDataDir !== persistentUserDataDir) {
1975
+ await (0, import_promises4.rm)(registration.filePath, {
1976
+ force: true
1977
+ }).catch(() => void 0);
1978
+ continue;
1979
+ }
1980
+ const runtimeMarker = await readRuntimeProfileCreationMarker(
1981
+ marker.runtimeUserDataDir
1982
+ );
1983
+ if (!runtimeMarker || runtimeMarker.persistentUserDataDir !== persistentUserDataDir || runtimeMarker.runtimeUserDataDir !== marker.runtimeUserDataDir) {
1984
+ await clearRuntimeProfileCreationState(
1985
+ marker.runtimeUserDataDir,
1986
+ persistentUserDataDir
1987
+ );
1988
+ continue;
1989
+ }
1990
+ if (await getProcessLiveness(runtimeMarker.creator) === "dead") {
1991
+ await clearRuntimeProfileCreationState(
1992
+ marker.runtimeUserDataDir,
1993
+ persistentUserDataDir
1994
+ );
1995
+ await (0, import_promises4.rm)(marker.runtimeUserDataDir, {
1996
+ recursive: true,
1997
+ force: true
1998
+ }).catch(() => void 0);
1999
+ continue;
2000
+ }
2001
+ hasLiveCreation = true;
2002
+ }
2003
+ return hasLiveCreation;
2004
+ }
2005
+ async function waitForRuntimeProfileCreationsToDrain(persistentUserDataDir) {
2006
+ while (true) {
2007
+ if (!await hasActiveRuntimeProfileCreations(persistentUserDataDir)) {
2008
+ return;
2009
+ }
2010
+ await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
2011
+ }
2012
+ }
2013
+ function parseRuntimeProfileCreationMarker(value) {
2014
+ if (!value || typeof value !== "object") {
2015
+ return null;
2016
+ }
2017
+ const parsed = value;
2018
+ const creator = parseProcessOwner(parsed.creator);
2019
+ const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
2020
+ const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
2021
+ const runtimeUserDataDir = typeof parsed.runtimeUserDataDir === "string" ? parsed.runtimeUserDataDir : void 0;
2022
+ if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || runtimeUserDataDir === void 0) {
2023
+ return null;
2024
+ }
2025
+ return {
2026
+ creator,
2027
+ persistentUserDataDir,
2028
+ profileDirectory,
2029
+ runtimeUserDataDir
2030
+ };
2031
+ }
2032
+ function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
2033
+ const remainder = ownedDirName.slice(ownedDirPrefix.length);
2034
+ const firstDashIndex = remainder.indexOf("-");
2035
+ const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
2036
+ if (firstDashIndex === -1 || secondDashIndex === -1) {
2037
+ return null;
2038
+ }
2039
+ const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
2040
+ const processStartedAtMs = Number.parseInt(
2041
+ remainder.slice(firstDashIndex + 1, secondDashIndex),
2042
+ 10
2043
+ );
2044
+ if (!Number.isInteger(pid) || pid <= 0) {
2045
+ return null;
2046
+ }
2047
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
2048
+ return null;
2049
+ }
2050
+ return { pid, processStartedAtMs };
2051
+ }
2052
+ async function sleep4(ms) {
2053
+ await new Promise((resolve) => setTimeout(resolve, ms));
2054
+ }
2055
+
2056
+ // src/browser/shared-real-browser-session.ts
2057
+ var import_node_crypto4 = require("crypto");
2058
+ var import_node_child_process3 = require("child_process");
2059
+ var import_promises5 = require("fs/promises");
2060
+ var import_node_net = require("net");
2061
+ var import_node_path5 = require("path");
2062
+ var import_playwright = require("playwright");
2063
+ var SHARED_SESSION_RETRY_DELAY_MS2 = 50;
2064
+ async function acquireSharedRealBrowserSession(options) {
2065
+ const reservation = await reserveSharedSessionClient(options);
2066
+ const sessionContext = await attachToSharedSession(reservation, options);
2067
+ let closed = false;
2068
+ return {
2069
+ browser: sessionContext.browser,
2070
+ context: sessionContext.context,
2071
+ page: sessionContext.page,
2072
+ close: async () => {
2073
+ if (closed) {
2074
+ return;
2075
+ }
2076
+ closed = true;
2077
+ await releaseSharedSessionClient(sessionContext);
2078
+ }
2079
+ };
2080
+ }
2081
+ function getOwnedRealBrowserProcessPolicy(platformName = process.platform) {
2082
+ if (platformName === "win32") {
2083
+ return {
2084
+ detached: false,
2085
+ killStrategy: "taskkill",
2086
+ shouldUnref: true
2087
+ };
2088
+ }
2089
+ if (platformName === "darwin") {
2090
+ return {
2091
+ detached: false,
2092
+ killStrategy: "process",
2093
+ shouldUnref: true
2094
+ };
2095
+ }
2096
+ return {
2097
+ detached: true,
2098
+ killStrategy: "process-group",
2099
+ shouldUnref: true
2100
+ };
2101
+ }
2102
+ async function reserveSharedSessionClient(options) {
2103
+ while (true) {
2104
+ const outcome = await withPersistentProfileControlLock(
2105
+ options.persistentProfile.userDataDir,
2106
+ async () => {
2107
+ if (await isPersistentProfileWriteLocked(
2108
+ options.persistentProfile.userDataDir
2109
+ )) {
2110
+ return { kind: "wait" };
2111
+ }
2112
+ if (await hasActiveRuntimeProfileCreations(
2113
+ options.persistentProfile.userDataDir
2114
+ )) {
2115
+ return { kind: "wait" };
2116
+ }
2117
+ return await withSharedSessionLock(
2118
+ options.persistentProfile.userDataDir,
2119
+ async () => {
2120
+ const state = await inspectSharedSessionState(options);
2121
+ if (state.kind === "wait") {
2122
+ return { kind: "wait" };
2123
+ }
2124
+ if (state.kind === "ready") {
2125
+ return {
2126
+ kind: "ready",
2127
+ reservation: await registerSharedSessionClient(
2128
+ options.persistentProfile.userDataDir,
2129
+ state.metadata
2130
+ )
2131
+ };
2132
+ }
2133
+ return {
2134
+ kind: "launch",
2135
+ reservation: await launchSharedSession(options)
2136
+ };
2137
+ }
2138
+ );
2139
+ }
2140
+ );
2141
+ if (outcome.kind === "wait") {
2142
+ await sleep5(SHARED_SESSION_RETRY_DELAY_MS2);
2143
+ continue;
2144
+ }
2145
+ if (outcome.kind === "ready") {
2146
+ return outcome.reservation;
2147
+ }
2148
+ try {
2149
+ await waitForSharedSessionReady(
2150
+ outcome.reservation.metadata,
2151
+ options.timeoutMs
2152
+ );
2153
+ } catch (error) {
2154
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2155
+ throw error;
2156
+ }
2157
+ try {
2158
+ return await withSharedSessionLock(
2159
+ options.persistentProfile.userDataDir,
2160
+ async () => {
2161
+ const metadata = await readSharedSessionMetadata(
2162
+ options.persistentProfile.userDataDir
2163
+ );
2164
+ if (!metadata || metadata.sessionId !== outcome.reservation.metadata.sessionId || !processOwnersEqual(
2165
+ metadata.browserOwner,
2166
+ outcome.reservation.launchedBrowserOwner
2167
+ )) {
2168
+ throw new Error(
2169
+ "The shared real-browser session changed before launch finalized."
2170
+ );
2171
+ }
2172
+ const readyMetadata = {
2173
+ ...metadata,
2174
+ state: "ready"
2175
+ };
2176
+ await writeSharedSessionMetadata(
2177
+ options.persistentProfile.userDataDir,
2178
+ readyMetadata
2179
+ );
2180
+ return await registerSharedSessionClient(
2181
+ options.persistentProfile.userDataDir,
2182
+ readyMetadata
2183
+ );
2184
+ }
2185
+ );
2186
+ } catch (error) {
2187
+ await cleanupFailedSharedSessionLaunch(outcome.reservation);
2188
+ throw error;
2189
+ }
2190
+ }
2191
+ }
2192
+ async function attachToSharedSession(reservation, options) {
2193
+ let browser = null;
2194
+ let page = null;
2195
+ try {
2196
+ const browserWsUrl = await resolveCdpWebSocketUrl(
2197
+ buildSharedSessionDiscoveryUrl(reservation.metadata.debugPort),
2198
+ options.timeoutMs
2199
+ );
2200
+ browser = await import_playwright.chromium.connectOverCDP(browserWsUrl, {
2201
+ timeout: options.timeoutMs
2202
+ });
2203
+ const context = getPrimaryBrowserContext(browser);
2204
+ page = await getSharedSessionPage(context, reservation.reuseExistingPage);
2205
+ if (options.initialUrl) {
2206
+ await page.goto(options.initialUrl, {
2207
+ timeout: options.timeoutMs,
2208
+ waitUntil: "domcontentloaded"
2209
+ });
2210
+ }
2211
+ return {
2212
+ browser,
2213
+ clientId: reservation.client.clientId,
2214
+ context,
2215
+ page,
2216
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2217
+ sessionId: reservation.metadata.sessionId
2218
+ };
2219
+ } catch (error) {
2220
+ if (page) {
2221
+ await page.close().catch(() => void 0);
2222
+ }
2223
+ if (browser) {
2224
+ await browser.close().catch(() => void 0);
2225
+ }
2226
+ await cleanupFailedSharedSessionAttach({
2227
+ clientId: reservation.client.clientId,
2228
+ persistentUserDataDir: reservation.metadata.persistentUserDataDir,
2229
+ sessionId: reservation.metadata.sessionId
2230
+ });
2231
+ throw error;
2232
+ }
2233
+ }
2234
+ async function releaseSharedSessionClient(context) {
2235
+ const releasePlan = await prepareSharedSessionCloseIfIdle(
2236
+ context.persistentUserDataDir,
2237
+ context.clientId,
2238
+ context.sessionId
2239
+ );
2240
+ if (releasePlan.closeBrowser) {
2241
+ await closeSharedSessionBrowser(
2242
+ context.persistentUserDataDir,
2243
+ releasePlan,
2244
+ context.browser
2245
+ );
2246
+ return;
2247
+ }
2248
+ await context.page.close().catch(() => void 0);
2249
+ await context.browser.close().catch(() => void 0);
2250
+ }
2251
+ async function inspectSharedSessionState(options) {
2252
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2253
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2254
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2255
+ if (!metadata) {
2256
+ if (liveClients.length > 0) {
2257
+ throw new Error(
2258
+ `Shared real-browser session metadata for "${persistentUserDataDir}" is missing while clients are still attached.`
2259
+ );
2260
+ }
2261
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
2262
+ force: true,
2263
+ recursive: true
2264
+ }).catch(() => void 0);
2265
+ return { kind: "missing" };
2266
+ }
2267
+ assertSharedSessionCompatibility(metadata, options);
2268
+ const browserState = await getProcessLiveness(metadata.browserOwner);
2269
+ if (browserState === "dead") {
2270
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
2271
+ force: true,
2272
+ recursive: true
2273
+ }).catch(() => void 0);
2274
+ return { kind: "missing" };
2275
+ }
2276
+ if (metadata.state === "ready") {
2277
+ return {
2278
+ kind: "ready",
2279
+ metadata
2280
+ };
2281
+ }
2282
+ const stateOwnerState = await getProcessLiveness(metadata.stateOwner);
2283
+ if (stateOwnerState === "dead") {
2284
+ const recoveredMetadata = {
2285
+ ...metadata,
2286
+ state: "ready"
2287
+ };
2288
+ await writeSharedSessionMetadata(persistentUserDataDir, recoveredMetadata);
2289
+ return {
2290
+ kind: "ready",
2291
+ metadata: recoveredMetadata
2292
+ };
2293
+ }
2294
+ return { kind: "wait" };
2295
+ }
2296
+ async function launchSharedSession(options) {
2297
+ const persistentUserDataDir = options.persistentProfile.userDataDir;
2298
+ await clearPersistentProfileSingletons(persistentUserDataDir);
2299
+ const debugPort = await reserveDebugPort();
2300
+ const launchArgs = buildRealBrowserLaunchArgs({
2301
+ debugPort,
2302
+ headless: options.headless,
2303
+ profileDirectory: options.profileDirectory,
2304
+ userDataDir: persistentUserDataDir
2305
+ });
2306
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2307
+ const processHandle = (0, import_node_child_process3.spawn)(options.executablePath, launchArgs, {
2308
+ detached: processPolicy.detached,
2309
+ stdio: "ignore"
2310
+ });
2311
+ if (processPolicy.shouldUnref) {
2312
+ processHandle.unref();
2313
+ }
2314
+ try {
2315
+ const browserOwner = await waitForSpawnedProcessOwner(
2316
+ processHandle.pid,
2317
+ options.timeoutMs
2318
+ );
2319
+ const metadata = {
2320
+ browserOwner,
2321
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2322
+ debugPort,
2323
+ executablePath: options.executablePath,
2324
+ headless: options.headless,
2325
+ persistentUserDataDir,
2326
+ profileDirectory: options.profileDirectory,
2327
+ sessionId: (0, import_node_crypto4.randomUUID)(),
2328
+ state: "launching",
2329
+ stateOwner: CURRENT_PROCESS_OWNER
2330
+ };
2331
+ await writeSharedSessionMetadata(persistentUserDataDir, metadata);
2332
+ return {
2333
+ launchedBrowserOwner: browserOwner,
2334
+ metadata
2335
+ };
2336
+ } catch (error) {
2337
+ await killSpawnedBrowserProcess(processHandle);
2338
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
2339
+ force: true,
2340
+ recursive: true
2341
+ }).catch(() => void 0);
2342
+ throw error;
2343
+ }
2344
+ }
2345
+ async function cleanupFailedSharedSessionLaunch(reservation) {
2346
+ const shouldPreserveLiveBrowser = await withSharedSessionLock(
2347
+ reservation.metadata.persistentUserDataDir,
2348
+ async () => {
2349
+ const metadata = await readSharedSessionMetadata(
2350
+ reservation.metadata.persistentUserDataDir
2351
+ );
2352
+ if (metadata && metadata.sessionId === reservation.metadata.sessionId && processOwnersEqual(
2353
+ metadata.browserOwner,
2354
+ reservation.launchedBrowserOwner
2355
+ )) {
2356
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2357
+ const readyMetadata = {
2358
+ ...metadata,
2359
+ state: "ready"
2360
+ };
2361
+ await writeSharedSessionMetadata(
2362
+ reservation.metadata.persistentUserDataDir,
2363
+ readyMetadata
2364
+ );
2365
+ return true;
2366
+ }
2367
+ await (0, import_promises5.rm)(
2368
+ buildSharedSessionDirPath(
2369
+ reservation.metadata.persistentUserDataDir
2370
+ ),
2371
+ {
2372
+ force: true,
2373
+ recursive: true
2374
+ }
2375
+ ).catch(() => void 0);
2376
+ }
2377
+ return false;
2378
+ }
2379
+ );
2380
+ if (shouldPreserveLiveBrowser) {
2381
+ return;
2382
+ }
2383
+ await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
2384
+ await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
2385
+ }
2386
+ async function cleanupFailedSharedSessionAttach(options) {
2387
+ const closePlan = await prepareSharedSessionCloseIfIdle(
2388
+ options.persistentUserDataDir,
2389
+ options.clientId,
2390
+ options.sessionId
2391
+ );
2392
+ if (!closePlan.closeBrowser) {
2393
+ return;
2394
+ }
2395
+ await closeSharedSessionBrowser(options.persistentUserDataDir, closePlan);
2396
+ }
2397
+ async function waitForSharedSessionReady(metadata, timeoutMs) {
2398
+ await resolveCdpWebSocketUrl(
2399
+ buildSharedSessionDiscoveryUrl(metadata.debugPort),
2400
+ timeoutMs
2401
+ );
2402
+ }
2403
+ function buildRealBrowserLaunchArgs(options) {
2404
+ const args = [
2405
+ `--user-data-dir=${options.userDataDir}`,
2406
+ `--profile-directory=${options.profileDirectory}`,
2407
+ `--remote-debugging-port=${options.debugPort}`,
2408
+ "--disable-blink-features=AutomationControlled"
2409
+ ];
2410
+ if (options.headless) {
2411
+ args.push("--headless=new");
2412
+ }
2413
+ return args;
2414
+ }
2415
+ async function requestBrowserShutdown(browser) {
2416
+ let session2 = null;
2417
+ try {
2418
+ session2 = await browser.newBrowserCDPSession();
2419
+ await session2.send("Browser.close");
2420
+ } catch {
2421
+ } finally {
2422
+ await session2?.detach().catch(() => void 0);
2423
+ }
2424
+ }
2425
+ async function killOwnedBrowserProcess(owner) {
2426
+ if (await getProcessLiveness(owner) === "dead") {
2427
+ return;
2428
+ }
2429
+ await killOwnedBrowserProcessByPid(owner.pid);
2430
+ }
2431
+ async function killSpawnedBrowserProcess(processHandle) {
2432
+ const pid = processHandle.pid;
2433
+ if (!pid || processHandle.exitCode !== null) {
2434
+ return;
2435
+ }
2436
+ await killOwnedBrowserProcessByPid(pid);
2437
+ await waitForPidToExit(pid, 2e3);
2438
+ }
2439
+ async function killOwnedBrowserProcessByPid(pid) {
2440
+ const processPolicy = getOwnedRealBrowserProcessPolicy();
2441
+ if (processPolicy.killStrategy === "taskkill") {
2442
+ await new Promise((resolve) => {
2443
+ const killer = (0, import_node_child_process3.spawn)(
2444
+ "taskkill",
2445
+ ["/pid", String(pid), "/t", "/f"],
2446
+ {
2447
+ stdio: "ignore"
2448
+ }
2449
+ );
2450
+ killer.on("error", () => resolve());
2451
+ killer.on("exit", () => resolve());
2452
+ });
2453
+ return;
2454
+ }
2455
+ if (processPolicy.killStrategy === "process-group") {
2456
+ try {
2457
+ process.kill(-pid, "SIGKILL");
2458
+ return;
2459
+ } catch {
2460
+ }
2461
+ }
2462
+ try {
2463
+ process.kill(pid, "SIGKILL");
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ async function waitForProcessToExit(owner, timeoutMs) {
2468
+ const deadline = Date.now() + timeoutMs;
2469
+ while (Date.now() < deadline) {
2470
+ if (await getProcessLiveness(owner) === "dead") {
2471
+ return;
2472
+ }
2473
+ await sleep5(50);
2474
+ }
2475
+ }
2476
+ async function waitForPidToExit(pid, timeoutMs) {
2477
+ const deadline = Date.now() + timeoutMs;
2478
+ while (Date.now() < deadline) {
2479
+ if (!isProcessRunning(pid)) {
2480
+ return;
2481
+ }
2482
+ await sleep5(50);
2483
+ }
2484
+ }
2485
+ async function waitForSpawnedProcessOwner(pid, timeoutMs) {
2486
+ if (!pid || pid <= 0) {
2487
+ throw new Error("Chrome did not expose a child process id.");
2488
+ }
2489
+ const deadline = Date.now() + timeoutMs;
2490
+ while (Date.now() < deadline) {
2491
+ const owner = await readProcessOwner(pid);
2492
+ if (owner) {
2493
+ return owner;
2494
+ }
2495
+ await sleep5(50);
2496
+ }
2497
+ throw new Error(
2498
+ `Chrome process ${pid} did not report a stable process start time.`
2499
+ );
2500
+ }
2501
+ async function withSharedSessionLock(persistentUserDataDir, action) {
2502
+ return await withDirLock(
2503
+ buildSharedSessionLockPath(persistentUserDataDir),
2504
+ action
2505
+ );
2506
+ }
2507
+ async function registerSharedSessionClient(persistentUserDataDir, metadata) {
2508
+ const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
2509
+ const client = buildSharedSessionClientRegistration();
2510
+ await (0, import_promises5.mkdir)(buildSharedSessionClientsDirPath(persistentUserDataDir), {
2511
+ recursive: true
2512
+ });
2513
+ await (0, import_promises5.writeFile)(
2514
+ buildSharedSessionClientPath(persistentUserDataDir, client.clientId),
2515
+ JSON.stringify(client, null, 2),
2516
+ {
2517
+ flag: "wx"
2518
+ }
2519
+ );
2520
+ return {
2521
+ client,
2522
+ metadata,
2523
+ reuseExistingPage: liveClients.length === 0
2524
+ };
2525
+ }
2526
+ async function removeSharedSessionClientRegistration(persistentUserDataDir, clientId) {
2527
+ await (0, import_promises5.rm)(buildSharedSessionClientPath(persistentUserDataDir, clientId), {
2528
+ force: true
2529
+ }).catch(() => void 0);
2530
+ }
2531
+ async function listLiveSharedSessionClients(persistentUserDataDir) {
2532
+ const clientsDirPath = buildSharedSessionClientsDirPath(persistentUserDataDir);
2533
+ let entries;
2534
+ try {
2535
+ entries = await (0, import_promises5.readdir)(clientsDirPath, {
2536
+ encoding: "utf8",
2537
+ withFileTypes: true
2538
+ });
2539
+ } catch {
2540
+ return [];
2541
+ }
2542
+ const liveClients = [];
2543
+ for (const entry of entries) {
2544
+ if (!entry.isFile()) {
2545
+ continue;
2546
+ }
2547
+ const filePath = (0, import_node_path5.join)(clientsDirPath, entry.name);
2548
+ const registration = await readSharedSessionClientRegistration(filePath);
2549
+ if (!registration) {
2550
+ await (0, import_promises5.rm)(filePath, { force: true }).catch(() => void 0);
2551
+ continue;
2552
+ }
2553
+ if (await getProcessLiveness(registration.owner) === "dead") {
2554
+ await (0, import_promises5.rm)(filePath, { force: true }).catch(() => void 0);
2555
+ continue;
2556
+ }
2557
+ liveClients.push(registration);
2558
+ }
2559
+ return liveClients;
2560
+ }
2561
+ async function readSharedSessionClientRegistration(filePath) {
2562
+ try {
2563
+ const raw = await (0, import_promises5.readFile)(filePath, "utf8");
2564
+ return parseSharedSessionClientRegistration(JSON.parse(raw));
2565
+ } catch {
2566
+ return null;
2567
+ }
2568
+ }
2569
+ function buildSharedSessionClientRegistration() {
2570
+ return {
2571
+ clientId: (0, import_node_crypto4.randomUUID)(),
2572
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2573
+ owner: CURRENT_PROCESS_OWNER
2574
+ };
2575
+ }
2576
+ function parseSharedSessionClientRegistration(value) {
2577
+ if (!value || typeof value !== "object") {
2578
+ return null;
2579
+ }
2580
+ const parsed = value;
2581
+ const owner = parseProcessOwner(parsed.owner);
2582
+ if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
2583
+ return null;
2584
+ }
2585
+ return {
2586
+ clientId: parsed.clientId,
2587
+ createdAt: parsed.createdAt,
2588
+ owner
2589
+ };
2590
+ }
2591
+ function assertSharedSessionCompatibility(metadata, options) {
2592
+ if (metadata.executablePath !== options.executablePath) {
2593
+ throw new Error(
2594
+ `Chrome profile "${options.profileDirectory}" is already running with executable "${metadata.executablePath}", not "${options.executablePath}".`
2595
+ );
2596
+ }
2597
+ if (metadata.headless !== options.headless) {
2598
+ throw new Error(
2599
+ `Chrome profile "${options.profileDirectory}" is already running with headless=${metadata.headless}, not ${options.headless}.`
2600
+ );
2601
+ }
2602
+ }
2603
+ async function prepareSharedSessionCloseIfIdle(persistentUserDataDir, clientId, sessionId) {
2604
+ return await withSharedSessionLock(persistentUserDataDir, async () => {
2605
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2606
+ await removeSharedSessionClientRegistration(
2607
+ persistentUserDataDir,
2608
+ clientId
2609
+ );
2610
+ if (!metadata || metadata.sessionId !== sessionId) {
2611
+ return {
2612
+ closeBrowser: false,
2613
+ sessionId
2614
+ };
2615
+ }
2616
+ const liveClients = await listLiveSharedSessionClients(
2617
+ persistentUserDataDir
2618
+ );
2619
+ if (liveClients.length > 0) {
2620
+ return {
2621
+ closeBrowser: false,
2622
+ sessionId: metadata.sessionId
2623
+ };
2624
+ }
2625
+ const closingMetadata = {
2626
+ ...metadata,
2627
+ state: "closing",
2628
+ stateOwner: CURRENT_PROCESS_OWNER
2629
+ };
2630
+ await writeSharedSessionMetadata(
2631
+ persistentUserDataDir,
2632
+ closingMetadata
2633
+ );
2634
+ return {
2635
+ browserOwner: closingMetadata.browserOwner,
2636
+ closeBrowser: true,
2637
+ sessionId: closingMetadata.sessionId
2638
+ };
2639
+ });
2640
+ }
2641
+ async function closeSharedSessionBrowser(persistentUserDataDir, closePlan, browser) {
2642
+ if (browser) {
2643
+ await requestBrowserShutdown(browser);
2644
+ await waitForProcessToExit(closePlan.browserOwner, 1e3);
2645
+ }
2646
+ if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
2647
+ await killOwnedBrowserProcess(closePlan.browserOwner);
2648
+ await waitForProcessToExit(closePlan.browserOwner, 2e3);
2649
+ }
2650
+ await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
2651
+ }
2652
+ async function finalizeSharedSessionClose(persistentUserDataDir, sessionId) {
2653
+ await withSharedSessionLock(persistentUserDataDir, async () => {
2654
+ const metadata = await readSharedSessionMetadata(persistentUserDataDir);
2655
+ if (!metadata || metadata.sessionId !== sessionId) {
2656
+ return;
2657
+ }
2658
+ const liveClients = await listLiveSharedSessionClients(
2659
+ persistentUserDataDir
2660
+ );
2661
+ if (liveClients.length > 0) {
2662
+ const readyMetadata = {
2663
+ ...metadata,
2664
+ state: "ready"
2665
+ };
2666
+ await writeSharedSessionMetadata(
2667
+ persistentUserDataDir,
2668
+ readyMetadata
2669
+ );
2670
+ return;
2671
+ }
2672
+ if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
2673
+ const readyMetadata = {
2674
+ ...metadata,
2675
+ state: "ready"
2676
+ };
2677
+ await writeSharedSessionMetadata(
2678
+ persistentUserDataDir,
2679
+ readyMetadata
2680
+ );
2681
+ return;
2682
+ }
2683
+ await (0, import_promises5.rm)(buildSharedSessionDirPath(persistentUserDataDir), {
2684
+ force: true,
2685
+ recursive: true
2686
+ }).catch(() => void 0);
2687
+ });
2688
+ }
2689
+ function getPrimaryBrowserContext(browser) {
2690
+ const contexts = browser.contexts();
2691
+ if (contexts.length === 0) {
2692
+ throw new Error(
2693
+ "Connection succeeded but no browser contexts were exposed."
2694
+ );
2695
+ }
2696
+ return contexts[0];
2697
+ }
2698
+ async function getSharedSessionPage(context, reuseExistingPage) {
2699
+ if (reuseExistingPage) {
2700
+ return await getExistingPageOrCreate(context);
2701
+ }
2702
+ return await context.newPage();
2703
+ }
2704
+ async function getExistingPageOrCreate(context) {
2705
+ const existingPage = context.pages()[0];
2706
+ if (existingPage) {
2707
+ return existingPage;
2708
+ }
2709
+ return await context.newPage();
2710
+ }
2711
+ function buildSharedSessionDiscoveryUrl(debugPort) {
2712
+ return `http://127.0.0.1:${debugPort}`;
2713
+ }
2714
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2715
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2716
+ return cdpUrl;
2717
+ }
2718
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2719
+ const deadline = Date.now() + timeoutMs;
2720
+ let lastError = "CDP discovery did not respond.";
2721
+ while (Date.now() < deadline) {
2722
+ const remaining = Math.max(deadline - Date.now(), 1e3);
2723
+ try {
2724
+ const response = await fetch(versionUrl, {
2725
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2726
+ });
2727
+ if (!response.ok) {
2728
+ lastError = `${response.status} ${response.statusText}`;
2729
+ } else {
2730
+ const payload = await response.json();
2731
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2732
+ if (wsUrl && wsUrl.trim()) {
2733
+ return wsUrl;
2734
+ }
2735
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
1137
2736
  }
1138
- await (0, import_promises.rm)((0, import_node_path.join)(profilesDir, entry.name), {
1139
- recursive: true,
1140
- force: true
1141
- }).catch(() => void 0);
1142
- })
1143
- );
1144
- }
1145
- function isTempDirOwnedByLiveProcess(tempDirName, tempDirPrefix) {
1146
- const owner = parseTempDirOwner(tempDirName, tempDirPrefix);
1147
- if (!owner) {
1148
- return false;
1149
- }
1150
- if (owner.pid === process.pid && Math.abs(owner.processStartedAtMs - PROCESS_STARTED_AT_MS) <= PROCESS_START_TIME_TOLERANCE_MS) {
1151
- return true;
2737
+ } catch (error) {
2738
+ lastError = error instanceof Error ? error.message : "Unknown error";
2739
+ }
2740
+ await sleep5(100);
1152
2741
  }
1153
- return isProcessRunning(owner.pid);
2742
+ throw new Error(
2743
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2744
+ );
1154
2745
  }
1155
- function parseTempDirOwner(tempDirName, tempDirPrefix) {
1156
- const remainder = tempDirName.slice(tempDirPrefix.length);
1157
- const firstDashIndex = remainder.indexOf("-");
1158
- const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
1159
- if (firstDashIndex === -1 || secondDashIndex === -1) {
1160
- return null;
2746
+ function normalizeDiscoveryUrl(cdpUrl) {
2747
+ let parsed;
2748
+ try {
2749
+ parsed = new URL(cdpUrl);
2750
+ } catch {
2751
+ throw new Error(
2752
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
2753
+ );
1161
2754
  }
1162
- const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
1163
- const processStartedAtMs = Number.parseInt(
1164
- remainder.slice(firstDashIndex + 1, secondDashIndex),
1165
- 10
1166
- );
1167
- if (!Number.isInteger(pid) || pid <= 0) {
1168
- return null;
2755
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2756
+ return parsed;
1169
2757
  }
1170
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
1171
- return null;
2758
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2759
+ throw new Error(
2760
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
2761
+ );
1172
2762
  }
1173
- return { pid, processStartedAtMs };
2763
+ const normalized = new URL(parsed.toString());
2764
+ normalized.pathname = "/json/version";
2765
+ normalized.search = "";
2766
+ normalized.hash = "";
2767
+ return normalized;
1174
2768
  }
1175
- function isProcessRunning(pid) {
1176
- try {
1177
- process.kill(pid, 0);
1178
- return true;
1179
- } catch (error) {
1180
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1181
- return code !== "ESRCH";
1182
- }
2769
+ async function reserveDebugPort() {
2770
+ return await new Promise((resolve, reject) => {
2771
+ const server2 = (0, import_node_net.createServer)();
2772
+ server2.unref();
2773
+ server2.on("error", reject);
2774
+ server2.listen(0, "127.0.0.1", () => {
2775
+ const address = server2.address();
2776
+ if (!address || typeof address === "string") {
2777
+ server2.close();
2778
+ reject(new Error("Failed to reserve a local debug port."));
2779
+ return;
2780
+ }
2781
+ server2.close((error) => {
2782
+ if (error) {
2783
+ reject(error);
2784
+ return;
2785
+ }
2786
+ resolve(address.port);
2787
+ });
2788
+ });
2789
+ });
2790
+ }
2791
+ async function sleep5(ms) {
2792
+ await new Promise((resolve) => setTimeout(resolve, ms));
1183
2793
  }
1184
2794
 
1185
2795
  // src/browser/pool.ts
1186
2796
  var BrowserPool = class {
1187
2797
  browser = null;
1188
- cdpProxy = null;
1189
- launchedProcess = null;
1190
- managedUserDataDir = null;
1191
- persistentProfile = false;
2798
+ activeSessionClose = null;
2799
+ closeInFlight = null;
1192
2800
  defaults;
1193
2801
  constructor(defaults = {}) {
1194
2802
  this.defaults = defaults;
1195
2803
  }
1196
2804
  async launch(options = {}) {
1197
- if (this.browser || this.cdpProxy || this.launchedProcess || this.managedUserDataDir) {
2805
+ if (this.browser || this.activeSessionClose) {
1198
2806
  await this.close();
1199
2807
  }
1200
2808
  const mode = options.mode ?? this.defaults.mode ?? "chromium";
@@ -1241,30 +2849,26 @@ var BrowserPool = class {
1241
2849
  return this.launchSandbox(options);
1242
2850
  }
1243
2851
  async close() {
1244
- const browser = this.browser;
1245
- const cdpProxy = this.cdpProxy;
1246
- const launchedProcess = this.launchedProcess;
1247
- const managedUserDataDir = this.managedUserDataDir;
1248
- const persistentProfile = this.persistentProfile;
1249
- this.browser = null;
1250
- this.cdpProxy = null;
1251
- this.launchedProcess = null;
1252
- this.managedUserDataDir = null;
1253
- this.persistentProfile = false;
2852
+ if (this.closeInFlight) {
2853
+ await this.closeInFlight;
2854
+ return;
2855
+ }
2856
+ const closeOperation = this.closeCurrent();
2857
+ this.closeInFlight = closeOperation;
1254
2858
  try {
1255
- if (browser) {
1256
- await browser.close().catch(() => void 0);
1257
- }
2859
+ await closeOperation;
2860
+ this.browser = null;
2861
+ this.activeSessionClose = null;
1258
2862
  } finally {
1259
- cdpProxy?.close();
1260
- await killProcessTree(launchedProcess);
1261
- if (managedUserDataDir && !persistentProfile) {
1262
- await (0, import_promises2.rm)(managedUserDataDir, {
1263
- recursive: true,
1264
- force: true
1265
- }).catch(() => void 0);
1266
- }
2863
+ this.closeInFlight = null;
2864
+ }
2865
+ }
2866
+ async closeCurrent() {
2867
+ if (this.activeSessionClose) {
2868
+ await this.activeSessionClose();
2869
+ return;
1267
2870
  }
2871
+ await this.browser?.close().catch(() => void 0);
1268
2872
  }
1269
2873
  async connectToRunning(cdpUrl, timeout) {
1270
2874
  let browser = null;
@@ -1279,11 +2883,14 @@ var BrowserPool = class {
1279
2883
  }
1280
2884
  cdpProxy = new CDPProxy(browserWsUrl, targetId);
1281
2885
  const proxyWsUrl = await cdpProxy.start();
1282
- browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
2886
+ browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
1283
2887
  timeout: timeout ?? 3e4
1284
2888
  });
1285
2889
  this.browser = browser;
1286
- this.cdpProxy = cdpProxy;
2890
+ this.activeSessionClose = async () => {
2891
+ await browser?.close().catch(() => void 0);
2892
+ cdpProxy?.close();
2893
+ };
1287
2894
  const { context, page } = await pickBrowserContextAndPage(browser);
1288
2895
  return { browser, context, page, isExternal: true };
1289
2896
  } catch (error) {
@@ -1292,7 +2899,7 @@ var BrowserPool = class {
1292
2899
  }
1293
2900
  cdpProxy?.close();
1294
2901
  this.browser = null;
1295
- this.cdpProxy = null;
2902
+ this.activeSessionClose = null;
1296
2903
  throw error;
1297
2904
  }
1298
2905
  }
@@ -1312,55 +2919,29 @@ var BrowserPool = class {
1312
2919
  sourceUserDataDir,
1313
2920
  profileDirectory
1314
2921
  );
1315
- await clearPersistentProfileSingletons(persistentProfile.userDataDir);
1316
- const debugPort = await reserveDebugPort();
1317
- const headless = resolveLaunchHeadless(
1318
- "real",
1319
- options.headless,
1320
- this.defaults.headless
1321
- );
1322
- const launchArgs = buildRealBrowserLaunchArgs({
1323
- userDataDir: persistentProfile.userDataDir,
2922
+ const sharedSession = await acquireSharedRealBrowserSession({
2923
+ executablePath,
2924
+ headless: resolveLaunchHeadless(
2925
+ "real",
2926
+ options.headless,
2927
+ this.defaults.headless
2928
+ ),
2929
+ initialUrl: options.initialUrl,
2930
+ persistentProfile,
1324
2931
  profileDirectory,
1325
- debugPort,
1326
- headless
2932
+ timeoutMs: options.timeout ?? 3e4
1327
2933
  });
1328
- const processHandle = (0, import_node_child_process.spawn)(executablePath, launchArgs, {
1329
- detached: process.platform !== "win32",
1330
- stdio: "ignore"
1331
- });
1332
- processHandle.unref();
1333
- let browser = null;
1334
- try {
1335
- const wsUrl = await resolveCdpWebSocketUrl(
1336
- `http://127.0.0.1:${debugPort}`,
1337
- options.timeout ?? 3e4
1338
- );
1339
- browser = await import_playwright.chromium.connectOverCDP(wsUrl, {
1340
- timeout: options.timeout ?? 3e4
1341
- });
1342
- const { context, page } = await createOwnedBrowserContextAndPage(
1343
- browser
1344
- );
1345
- if (options.initialUrl) {
1346
- await page.goto(options.initialUrl, {
1347
- waitUntil: "domcontentloaded",
1348
- timeout: options.timeout ?? 3e4
1349
- });
1350
- }
1351
- this.browser = browser;
1352
- this.launchedProcess = processHandle;
1353
- this.managedUserDataDir = persistentProfile.userDataDir;
1354
- this.persistentProfile = true;
1355
- return { browser, context, page, isExternal: false };
1356
- } catch (error) {
1357
- await browser?.close().catch(() => void 0);
1358
- await killProcessTree(processHandle);
1359
- throw error;
1360
- }
2934
+ this.browser = sharedSession.browser;
2935
+ this.activeSessionClose = sharedSession.close;
2936
+ return {
2937
+ browser: sharedSession.browser,
2938
+ context: sharedSession.context,
2939
+ page: sharedSession.page,
2940
+ isExternal: false
2941
+ };
1361
2942
  }
1362
2943
  async launchSandbox(options) {
1363
- const browser = await import_playwright.chromium.launch({
2944
+ const browser = await import_playwright2.chromium.launch({
1364
2945
  headless: resolveLaunchHeadless(
1365
2946
  "chromium",
1366
2947
  options.headless,
@@ -1373,11 +2954,14 @@ var BrowserPool = class {
1373
2954
  const context = await browser.newContext(options.context || {});
1374
2955
  const page = await context.newPage();
1375
2956
  this.browser = browser;
2957
+ this.activeSessionClose = async () => {
2958
+ await browser.close().catch(() => void 0);
2959
+ };
1376
2960
  return { browser, context, page, isExternal: false };
1377
2961
  }
1378
2962
  };
1379
2963
  async function pickBrowserContextAndPage(browser) {
1380
- const context = getPrimaryBrowserContext(browser);
2964
+ const context = getPrimaryBrowserContext2(browser);
1381
2965
  const page = await getAttachedPageOrCreate(context);
1382
2966
  return { context, page };
1383
2967
  }
@@ -1390,11 +2974,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
1390
2974
  }
1391
2975
  return mode === "real";
1392
2976
  }
1393
- async function createOwnedBrowserContextAndPage(browser) {
1394
- const context = getPrimaryBrowserContext(browser);
1395
- const page = await getExistingPageOrCreate(context);
1396
- return { context, page };
1397
- }
1398
2977
  async function getAttachedPageOrCreate(context) {
1399
2978
  const pages = context.pages();
1400
2979
  const inspectablePage = pages.find(
@@ -1409,14 +2988,7 @@ async function getAttachedPageOrCreate(context) {
1409
2988
  }
1410
2989
  return await context.newPage();
1411
2990
  }
1412
- async function getExistingPageOrCreate(context) {
1413
- const existingPage = context.pages()[0];
1414
- if (existingPage) {
1415
- return existingPage;
1416
- }
1417
- return await context.newPage();
1418
- }
1419
- function getPrimaryBrowserContext(browser) {
2991
+ function getPrimaryBrowserContext2(browser) {
1420
2992
  const contexts = browser.contexts();
1421
2993
  if (contexts.length === 0) {
1422
2994
  throw new Error(
@@ -1435,125 +3007,6 @@ function safePageUrl(page) {
1435
3007
  return "";
1436
3008
  }
1437
3009
  }
1438
- function normalizeDiscoveryUrl(cdpUrl) {
1439
- let parsed;
1440
- try {
1441
- parsed = new URL(cdpUrl);
1442
- } catch {
1443
- throw new Error(
1444
- `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1445
- );
1446
- }
1447
- if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
1448
- return parsed;
1449
- }
1450
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1451
- throw new Error(
1452
- `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
1453
- );
1454
- }
1455
- const normalized = new URL(parsed.toString());
1456
- normalized.pathname = "/json/version";
1457
- normalized.search = "";
1458
- normalized.hash = "";
1459
- return normalized;
1460
- }
1461
- async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
1462
- if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
1463
- return cdpUrl;
1464
- }
1465
- const versionUrl = normalizeDiscoveryUrl(cdpUrl);
1466
- const deadline = Date.now() + timeoutMs;
1467
- let lastError = "CDP discovery did not respond.";
1468
- while (Date.now() < deadline) {
1469
- const remaining = Math.max(deadline - Date.now(), 1e3);
1470
- try {
1471
- const response = await fetch(versionUrl, {
1472
- signal: AbortSignal.timeout(Math.min(remaining, 5e3))
1473
- });
1474
- if (!response.ok) {
1475
- lastError = `${response.status} ${response.statusText}`;
1476
- } else {
1477
- const payload = await response.json();
1478
- const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
1479
- if (wsUrl && wsUrl.trim()) {
1480
- return wsUrl;
1481
- }
1482
- lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
1483
- }
1484
- } catch (error) {
1485
- lastError = error instanceof Error ? error.message : "Unknown error";
1486
- }
1487
- await sleep(100);
1488
- }
1489
- throw new Error(
1490
- `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1491
- );
1492
- }
1493
- async function reserveDebugPort() {
1494
- return await new Promise((resolve, reject) => {
1495
- const server2 = (0, import_node_net.createServer)();
1496
- server2.unref();
1497
- server2.on("error", reject);
1498
- server2.listen(0, "127.0.0.1", () => {
1499
- const address = server2.address();
1500
- if (!address || typeof address === "string") {
1501
- server2.close();
1502
- reject(new Error("Failed to reserve a local debug port."));
1503
- return;
1504
- }
1505
- server2.close((error) => {
1506
- if (error) {
1507
- reject(error);
1508
- return;
1509
- }
1510
- resolve(address.port);
1511
- });
1512
- });
1513
- });
1514
- }
1515
- function buildRealBrowserLaunchArgs(options) {
1516
- const args = [
1517
- `--user-data-dir=${options.userDataDir}`,
1518
- `--profile-directory=${options.profileDirectory}`,
1519
- `--remote-debugging-port=${options.debugPort}`,
1520
- "--disable-blink-features=AutomationControlled"
1521
- ];
1522
- if (options.headless) {
1523
- args.push("--headless=new");
1524
- }
1525
- return args;
1526
- }
1527
- async function killProcessTree(processHandle) {
1528
- if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
1529
- return;
1530
- }
1531
- if (process.platform === "win32") {
1532
- await new Promise((resolve) => {
1533
- const killer = (0, import_node_child_process.spawn)(
1534
- "taskkill",
1535
- ["/pid", String(processHandle.pid), "/t", "/f"],
1536
- {
1537
- stdio: "ignore"
1538
- }
1539
- );
1540
- killer.on("error", () => resolve());
1541
- killer.on("exit", () => resolve());
1542
- });
1543
- return;
1544
- }
1545
- try {
1546
- process.kill(-processHandle.pid, "SIGKILL");
1547
- } catch {
1548
- try {
1549
- processHandle.kill("SIGKILL");
1550
- } catch {
1551
- }
1552
- }
1553
- }
1554
- async function sleep(ms) {
1555
- await new Promise((resolve) => setTimeout(resolve, ms));
1556
- }
1557
3010
 
1558
3011
  // src/config.ts
1559
3012
  var import_fs2 = __toESM(require("fs"), 1);
@@ -1860,9 +3313,9 @@ function resolveNamespaceDir(rootDir, namespace) {
1860
3313
  const selectorsRoot = import_path2.default.resolve(rootDir, ".opensteer", "selectors");
1861
3314
  const normalizedNamespace = normalizeNamespace(namespace);
1862
3315
  const namespaceDir = import_path2.default.resolve(selectorsRoot, normalizedNamespace);
1863
- const relative = import_path2.default.relative(selectorsRoot, namespaceDir);
1864
- if (relative === "" || relative === ".") return namespaceDir;
1865
- if (relative.startsWith("..") || import_path2.default.isAbsolute(relative)) {
3316
+ const relative2 = import_path2.default.relative(selectorsRoot, namespaceDir);
3317
+ if (relative2 === "" || relative2 === ".") return namespaceDir;
3318
+ if (relative2.startsWith("..") || import_path2.default.isAbsolute(relative2)) {
1866
3319
  throw new Error(
1867
3320
  `Namespace "${namespace}" resolves outside selectors root.`
1868
3321
  );
@@ -2419,8 +3872,8 @@ function resolveNamespace(config, rootDir) {
2419
3872
  }
2420
3873
  const caller = getCallerFilePath();
2421
3874
  if (!caller) return normalizeNamespace("default");
2422
- const relative = import_path3.default.relative(rootDir, caller);
2423
- const cleaned = relative.replace(/\\/g, "/").replace(/\.(ts|tsx|js|mjs|cjs)$/, "");
3875
+ const relative2 = import_path3.default.relative(rootDir, caller);
3876
+ const cleaned = relative2.replace(/\\/g, "/").replace(/\.(ts|tsx|js|mjs|cjs)$/, "");
2424
3877
  return normalizeNamespace(cleaned || "default");
2425
3878
  }
2426
3879
  function getCallerFilePath() {
@@ -2825,7 +4278,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2825
4278
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
2826
4279
  Math.max(0, deadline - Date.now())
2827
4280
  );
2828
- await sleep2(retryDelay);
4281
+ await sleep6(retryDelay);
2829
4282
  }
2830
4283
  }
2831
4284
  }
@@ -2858,7 +4311,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2858
4311
  () => ({ kind: "resolved" }),
2859
4312
  (error) => ({ kind: "rejected", error })
2860
4313
  );
2861
- const timeoutPromise = sleep2(
4314
+ const timeoutPromise = sleep6(
2862
4315
  timeout + FRAME_EVALUATE_GRACE_MS
2863
4316
  ).then(() => ({ kind: "timeout" }));
2864
4317
  const result = await Promise.race([
@@ -3000,7 +4453,7 @@ function isIgnorableFrameError(error) {
3000
4453
  const message = error.message;
3001
4454
  return message.includes("Frame was detached") || message.includes("Target page, context or browser has been closed") || isTransientExecutionContextError(error) || message.includes("No frame for given id found");
3002
4455
  }
3003
- function sleep2(ms) {
4456
+ function sleep6(ms) {
3004
4457
  return new Promise((resolve) => {
3005
4458
  setTimeout(resolve, ms);
3006
4459
  });
@@ -7251,7 +8704,7 @@ async function closeTab(context, activePage, index) {
7251
8704
  }
7252
8705
 
7253
8706
  // src/actions/cookies.ts
7254
- var import_promises3 = require("fs/promises");
8707
+ var import_promises6 = require("fs/promises");
7255
8708
  async function getCookies(context, url) {
7256
8709
  return context.cookies(url ? [url] : void 0);
7257
8710
  }
@@ -7263,10 +8716,10 @@ async function clearCookies(context) {
7263
8716
  }
7264
8717
  async function exportCookies(context, filePath, url) {
7265
8718
  const cookies = await context.cookies(url ? [url] : void 0);
7266
- await (0, import_promises3.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
8719
+ await (0, import_promises6.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
7267
8720
  }
7268
8721
  async function importCookies(context, filePath) {
7269
- const raw = await (0, import_promises3.readFile)(filePath, "utf-8");
8722
+ const raw = await (0, import_promises6.readFile)(filePath, "utf-8");
7270
8723
  const cookies = JSON.parse(raw);
7271
8724
  await context.addCookies(cookies);
7272
8725
  }
@@ -7577,7 +9030,7 @@ var AdaptiveNetworkTracker = class {
7577
9030
  this.idleSince = 0;
7578
9031
  }
7579
9032
  const remaining = Math.max(1, options.deadline - now);
7580
- await sleep3(Math.min(NETWORK_POLL_MS, remaining));
9033
+ await sleep7(Math.min(NETWORK_POLL_MS, remaining));
7581
9034
  }
7582
9035
  }
7583
9036
  handleRequestStarted = (request) => {
@@ -7622,7 +9075,7 @@ var AdaptiveNetworkTracker = class {
7622
9075
  return false;
7623
9076
  }
7624
9077
  };
7625
- async function sleep3(ms) {
9078
+ async function sleep7(ms) {
7626
9079
  await new Promise((resolve) => {
7627
9080
  setTimeout(resolve, ms);
7628
9081
  });
@@ -9324,13 +10777,13 @@ function dedupeNewest(entries) {
9324
10777
  }
9325
10778
 
9326
10779
  // src/cloud/cdp-client.ts
9327
- var import_playwright2 = require("playwright");
10780
+ var import_playwright3 = require("playwright");
9328
10781
  var CloudCdpClient = class {
9329
10782
  async connect(args) {
9330
10783
  const endpoint = withTokenQuery(args.wsUrl, args.token);
9331
10784
  let browser;
9332
10785
  try {
9333
- browser = await import_playwright2.chromium.connectOverCDP(endpoint);
10786
+ browser = await import_playwright3.chromium.connectOverCDP(endpoint);
9334
10787
  } catch (error) {
9335
10788
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
9336
10789
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -11147,7 +12600,7 @@ async function executeAgentAction(page, action) {
11147
12600
  }
11148
12601
  case "wait": {
11149
12602
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
11150
- await sleep4(ms);
12603
+ await sleep8(ms);
11151
12604
  return;
11152
12605
  }
11153
12606
  case "goto": {
@@ -11312,7 +12765,7 @@ async function pressKeyCombo(page, combo) {
11312
12765
  }
11313
12766
  }
11314
12767
  }
11315
- function sleep4(ms) {
12768
+ function sleep8(ms) {
11316
12769
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11317
12770
  }
11318
12771
 
@@ -11343,7 +12796,7 @@ var OpensteerCuaAgentHandler = class {
11343
12796
  if (isMutatingAgentAction(action)) {
11344
12797
  this.onMutatingAction?.(action);
11345
12798
  }
11346
- await sleep5(this.config.waitBetweenActionsMs);
12799
+ await sleep9(this.config.waitBetweenActionsMs);
11347
12800
  });
11348
12801
  try {
11349
12802
  const result = await this.client.execute({
@@ -11405,7 +12858,7 @@ var OpensteerCuaAgentHandler = class {
11405
12858
  await this.cursorController.preview({ x, y }, "agent");
11406
12859
  }
11407
12860
  };
11408
- function sleep5(ms) {
12861
+ function sleep9(ms) {
11409
12862
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11410
12863
  }
11411
12864
 
@@ -11841,7 +13294,7 @@ var CursorController = class {
11841
13294
  for (const step of motion.points) {
11842
13295
  await this.renderer.move(step, this.style);
11843
13296
  if (motion.stepDelayMs > 0) {
11844
- await sleep6(motion.stepDelayMs);
13297
+ await sleep10(motion.stepDelayMs);
11845
13298
  }
11846
13299
  }
11847
13300
  if (shouldPulse(intent)) {
@@ -11999,7 +13452,7 @@ function clamp2(value, min, max) {
11999
13452
  function shouldPulse(intent) {
12000
13453
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
12001
13454
  }
12002
- function sleep6(ms) {
13455
+ function sleep10(ms) {
12003
13456
  return new Promise((resolve) => setTimeout(resolve, ms));
12004
13457
  }
12005
13458
 
@@ -12473,15 +13926,22 @@ var Opensteer = class _Opensteer {
12473
13926
  }
12474
13927
  return;
12475
13928
  }
12476
- if (this.ownsBrowser) {
12477
- await this.pool.close();
12478
- }
12479
- this.browser = null;
12480
- this.pageRef = null;
12481
- this.contextRef = null;
12482
- this.ownsBrowser = false;
12483
- if (this.cursorController) {
12484
- await this.cursorController.dispose().catch(() => void 0);
13929
+ let closedOwnedBrowser = false;
13930
+ try {
13931
+ if (this.ownsBrowser) {
13932
+ await this.pool.close();
13933
+ closedOwnedBrowser = true;
13934
+ }
13935
+ } finally {
13936
+ this.browser = null;
13937
+ this.pageRef = null;
13938
+ this.contextRef = null;
13939
+ if (!this.ownsBrowser || closedOwnedBrowser) {
13940
+ this.ownsBrowser = false;
13941
+ }
13942
+ if (this.cursorController) {
13943
+ await this.cursorController.dispose().catch(() => void 0);
13944
+ }
12485
13945
  }
12486
13946
  }
12487
13947
  async syncLocalSelectorCacheToCloud() {
@@ -14825,7 +16285,7 @@ function getMetadataPath(session2) {
14825
16285
  }
14826
16286
 
14827
16287
  // src/cli/commands.ts
14828
- var import_promises4 = require("fs/promises");
16288
+ var import_promises7 = require("fs/promises");
14829
16289
  var commands = {
14830
16290
  async navigate(opensteer, args) {
14831
16291
  const url = args.url;
@@ -14879,7 +16339,7 @@ var commands = {
14879
16339
  fullPage: args.fullPage,
14880
16340
  type
14881
16341
  });
14882
- await (0, import_promises4.writeFile)(file, buffer);
16342
+ await (0, import_promises7.writeFile)(file, buffer);
14883
16343
  return { file };
14884
16344
  },
14885
16345
  async click(opensteer, args) {