opensteer 0.6.12 → 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_process2 = 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,669 +917,247 @@ 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
- var import_node_child_process = require("child_process");
925
933
  var import_node_fs = require("fs");
926
- var import_promises = require("fs/promises");
927
- var import_node_os = require("os");
934
+ var import_promises2 = require("fs/promises");
928
935
  var import_node_path = require("path");
936
+
937
+ // src/browser/process-owner.ts
938
+ var import_node_child_process = require("child_process");
939
+ var import_promises = require("fs/promises");
929
940
  var import_node_util = require("util");
930
941
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
931
- var OPENSTEER_META_FILE = ".opensteer-meta.json";
932
- var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
933
- var LOCK_OWNER_FILE = "owner.json";
934
- var LOCK_RECLAIMER_DIR = "reclaimer";
935
942
  var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
936
943
  var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
937
- var PROFILE_LOCK_RETRY_DELAY_MS = 50;
944
+ var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
938
945
  var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
939
946
  var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
940
- var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
941
- "SingletonCookie",
942
- "SingletonLock",
943
- "SingletonSocket",
944
- "DevToolsActivePort",
945
- "lockfile"
946
- ]);
947
- var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
948
- ...CHROME_SINGLETON_ENTRIES,
949
- OPENSTEER_META_FILE,
950
- OPENSTEER_RUNTIME_META_FILE
951
- ]);
952
- var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
953
- "Crash Reports",
954
- "Crashpad",
955
- "BrowserMetrics",
956
- "GrShaderCache",
957
- "ShaderCache",
958
- "GraphiteDawnCache",
959
- "component_crx_cache",
960
- "Crowd Deny",
961
- "hyphen-data",
962
- "OnDeviceHeadSuggestModel",
963
- "OptimizationGuidePredictionModels",
964
- "Segmentation Platform",
965
- "SmartCardDeviceNames",
966
- "WidevineCdm",
967
- "pnacl"
968
- ]);
969
- var CURRENT_PROCESS_LOCK_OWNER = {
947
+ var CURRENT_PROCESS_OWNER = {
970
948
  pid: process.pid,
971
949
  processStartedAtMs: PROCESS_STARTED_AT_MS
972
950
  };
973
951
  var linuxClockTicksPerSecondPromise = null;
974
- async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
975
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
976
- const targetUserDataDir = (0, import_node_path.join)(
977
- expandHome(profilesRootDir),
978
- buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
979
- );
980
- const sourceProfileDir = (0, import_node_path.join)(resolvedSourceUserDataDir, profileDirectory);
981
- const metadata = buildPersistentProfileMetadata(
982
- resolvedSourceUserDataDir,
983
- profileDirectory
984
- );
985
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(targetUserDataDir), { recursive: true });
986
- return await withPersistentProfileLock(targetUserDataDir, async () => {
987
- await cleanOrphanedOwnedDirs(
988
- (0, import_node_path.dirname)(targetUserDataDir),
989
- buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
990
- );
991
- if (!(0, import_node_fs.existsSync)(sourceProfileDir)) {
992
- throw new Error(
993
- `Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
994
- );
995
- }
996
- const created = await createPersistentProfileClone(
997
- resolvedSourceUserDataDir,
998
- sourceProfileDir,
999
- targetUserDataDir,
1000
- profileDirectory,
1001
- metadata
1002
- );
1003
- await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
1004
- return {
1005
- created,
1006
- userDataDir: targetUserDataDir
1007
- };
1008
- });
1009
- }
1010
- async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
1011
- const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
1012
- const runtimeRootDir = expandHome(runtimesRootDir);
1013
- await (0, import_promises.mkdir)(runtimeRootDir, { recursive: true });
1014
- return await withPersistentProfileLock(
1015
- resolvedSourceUserDataDir,
1016
- async () => {
1017
- const sourceMetadata = await readPersistentProfileMetadata(
1018
- resolvedSourceUserDataDir
1019
- );
1020
- await cleanOrphanedOwnedDirs(
1021
- runtimeRootDir,
1022
- buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
1023
- );
1024
- const runtimeUserDataDir = await (0, import_promises.mkdtemp)(
1025
- buildRuntimeProfileDirPrefix(
1026
- runtimeRootDir,
1027
- resolvedSourceUserDataDir
1028
- )
1029
- );
1030
- try {
1031
- await copyUserDataDirSnapshot(
1032
- resolvedSourceUserDataDir,
1033
- runtimeUserDataDir
1034
- );
1035
- await writeRuntimeProfileMetadata(
1036
- runtimeUserDataDir,
1037
- await buildRuntimeProfileMetadata(
1038
- runtimeUserDataDir,
1039
- sourceMetadata?.profileDirectory ?? null
1040
- )
1041
- );
1042
- return {
1043
- persistentUserDataDir: resolvedSourceUserDataDir,
1044
- userDataDir: runtimeUserDataDir
1045
- };
1046
- } catch (error) {
1047
- await (0, import_promises.rm)(runtimeUserDataDir, {
1048
- recursive: true,
1049
- force: true
1050
- }).catch(() => void 0);
1051
- throw error;
1052
- }
1053
- }
1054
- );
1055
- }
1056
- async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
1057
- const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
1058
- const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
1059
- await withPersistentProfileLock(resolvedPersistentUserDataDir, async () => {
1060
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(resolvedPersistentUserDataDir), { recursive: true });
1061
- await cleanOrphanedOwnedDirs(
1062
- (0, import_node_path.dirname)(resolvedPersistentUserDataDir),
1063
- buildPersistentProfileTempDirNamePrefix(resolvedPersistentUserDataDir)
1064
- );
1065
- const metadata = await requirePersistentProfileMetadata(
1066
- resolvedPersistentUserDataDir
1067
- );
1068
- const runtimeMetadata = await requireRuntimeProfileMetadata(
1069
- resolvedRuntimeUserDataDir,
1070
- metadata.profileDirectory
1071
- );
1072
- await mergePersistentProfileSnapshot(
1073
- resolvedRuntimeUserDataDir,
1074
- resolvedPersistentUserDataDir,
1075
- metadata,
1076
- runtimeMetadata
1077
- );
1078
- });
1079
- await (0, import_promises.rm)(resolvedRuntimeUserDataDir, {
1080
- recursive: true,
1081
- force: true
1082
- });
1083
- }
1084
- function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
1085
- const hash = (0, import_node_crypto.createHash)("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
1086
- const sourceLabel = sanitizePathSegment((0, import_node_path.basename)(sourceUserDataDir) || "user-data");
1087
- const profileLabel = sanitizePathSegment(profileDirectory || "Default");
1088
- return `${sourceLabel}-${profileLabel}-${hash}`;
1089
- }
1090
- function defaultPersistentProfilesRootDir() {
1091
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".opensteer", "real-browser-profiles");
1092
- }
1093
- function defaultRuntimeProfilesRootDir() {
1094
- return (0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-real-browser-runtimes");
1095
- }
1096
- function sanitizePathSegment(value) {
1097
- const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
1098
- return sanitized.replace(/^-|-$/g, "") || "profile";
1099
- }
1100
- function isProfileDirectory(userDataDir, entry) {
1101
- return (0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, entry, "Preferences"));
1102
- }
1103
- async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
1104
- await (0, import_promises.cp)(sourceUserDataDir, targetUserDataDir, {
1105
- recursive: true,
1106
- filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
1107
- });
1108
- }
1109
- function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
1110
- const candidateRelativePath = (0, import_node_path.relative)(userDataDir, candidatePath);
1111
- if (!candidateRelativePath) {
1112
- return true;
1113
- }
1114
- const segments = candidateRelativePath.split(import_node_path.sep).filter(Boolean);
1115
- if (segments.length !== 1) {
1116
- return true;
952
+ function parseProcessOwner(value) {
953
+ if (!value || typeof value !== "object") {
954
+ return null;
1117
955
  }
1118
- return !COPY_SKIP_ENTRIES.has(segments[0]);
1119
- }
1120
- async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
1121
- let entries;
1122
- try {
1123
- entries = await (0, import_promises.readdir)(sourceUserDataDir);
1124
- } catch {
1125
- return;
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;
1126
961
  }
1127
- const copyTasks = [];
1128
- for (const entry of entries) {
1129
- if (COPY_SKIP_ENTRIES.has(entry)) continue;
1130
- if (entry === targetProfileDirectory) continue;
1131
- const sourcePath = (0, import_node_path.join)(sourceUserDataDir, entry);
1132
- const targetPath = (0, import_node_path.join)(targetUserDataDir, entry);
1133
- if ((0, import_node_fs.existsSync)(targetPath)) continue;
1134
- let entryStat;
1135
- try {
1136
- entryStat = await (0, import_promises.stat)(sourcePath);
1137
- } catch {
1138
- continue;
1139
- }
1140
- if (entryStat.isFile()) {
1141
- copyTasks.push((0, import_promises.copyFile)(sourcePath, targetPath).catch(() => void 0));
1142
- } else if (entryStat.isDirectory()) {
1143
- if (isProfileDirectory(sourceUserDataDir, entry)) continue;
1144
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
1145
- copyTasks.push(
1146
- (0, import_promises.cp)(sourcePath, targetPath, { recursive: true }).catch(
1147
- () => void 0
1148
- )
1149
- );
1150
- }
962
+ if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
963
+ return null;
1151
964
  }
1152
- await Promise.all(copyTasks);
1153
- }
1154
- async function writePersistentProfileMetadata(userDataDir, metadata) {
1155
- await (0, import_promises.writeFile)(
1156
- (0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE),
1157
- JSON.stringify(metadata, null, 2)
1158
- );
1159
- }
1160
- function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1161
965
  return {
1162
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1163
- profileDirectory,
1164
- source: sourceUserDataDir
966
+ pid,
967
+ processStartedAtMs
1165
968
  };
1166
969
  }
1167
- async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1168
- if ((0, import_node_fs.existsSync)(targetUserDataDir)) {
1169
- return false;
1170
- }
1171
- const tempUserDataDir = await (0, import_promises.mkdtemp)(
1172
- buildPersistentProfileTempDirPrefix(targetUserDataDir)
1173
- );
1174
- let published = false;
1175
- try {
1176
- await materializePersistentProfileSnapshot(
1177
- sourceUserDataDir,
1178
- sourceProfileDir,
1179
- tempUserDataDir,
1180
- profileDirectory,
1181
- metadata
1182
- );
1183
- try {
1184
- await (0, import_promises.rename)(tempUserDataDir, targetUserDataDir);
1185
- } catch (error) {
1186
- if (wasDirPublishedByAnotherProcess(error, targetUserDataDir)) {
1187
- return false;
1188
- }
1189
- throw error;
1190
- }
1191
- published = true;
1192
- return true;
1193
- } finally {
1194
- if (!published) {
1195
- await (0, import_promises.rm)(tempUserDataDir, {
1196
- recursive: true,
1197
- force: true
1198
- }).catch(() => void 0);
1199
- }
970
+ function processOwnersEqual(left, right) {
971
+ if (!left || !right) {
972
+ return left === right;
1200
973
  }
974
+ return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1201
975
  }
1202
- async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
1203
- if (!(0, import_node_fs.existsSync)(sourceProfileDir)) {
1204
- throw new Error(
1205
- `Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
1206
- );
976
+ async function getProcessLiveness(owner) {
977
+ if (owner.pid === process.pid && hasMatchingProcessStartTime(
978
+ owner.processStartedAtMs,
979
+ PROCESS_STARTED_AT_MS
980
+ )) {
981
+ return "live";
1207
982
  }
1208
- await (0, import_promises.cp)(sourceProfileDir, (0, import_node_path.join)(targetUserDataDir, profileDirectory), {
1209
- recursive: true
1210
- });
1211
- await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
1212
- await writePersistentProfileMetadata(targetUserDataDir, metadata);
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";
1213
991
  }
1214
- async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
1215
- const tempUserDataDir = await (0, import_promises.mkdtemp)(
1216
- buildPersistentProfileTempDirPrefix(persistentUserDataDir)
1217
- );
1218
- let published = false;
992
+ function isProcessRunning(pid) {
1219
993
  try {
1220
- const baseEntries = deserializeSnapshotManifestEntries(
1221
- runtimeMetadata.baseEntries
1222
- );
1223
- const currentEntries = await collectPersistentSnapshotEntries(
1224
- persistentUserDataDir,
1225
- metadata.profileDirectory
1226
- );
1227
- const runtimeEntries = await collectPersistentSnapshotEntries(
1228
- runtimeUserDataDir,
1229
- metadata.profileDirectory
1230
- );
1231
- const mergedEntries = resolveMergedSnapshotEntries(
1232
- baseEntries,
1233
- currentEntries,
1234
- runtimeEntries
1235
- );
1236
- await materializeMergedPersistentProfileSnapshot(
1237
- tempUserDataDir,
1238
- currentEntries,
1239
- runtimeEntries,
1240
- mergedEntries
1241
- );
1242
- await writePersistentProfileMetadata(tempUserDataDir, metadata);
1243
- await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
1244
- published = true;
1245
- } finally {
1246
- if (!published) {
1247
- await (0, import_promises.rm)(tempUserDataDir, {
1248
- recursive: true,
1249
- force: true
1250
- }).catch(() => void 0);
1251
- }
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";
1252
999
  }
1253
1000
  }
1254
- async function buildRuntimeProfileMetadata(runtimeUserDataDir, profileDirectory) {
1255
- if (!profileDirectory) {
1256
- return {
1257
- baseEntries: {},
1258
- profileDirectory: null
1259
- };
1001
+ async function readProcessOwner(pid) {
1002
+ const processStartedAtMs = await readProcessStartedAtMs(pid);
1003
+ if (processStartedAtMs === null) {
1004
+ return null;
1260
1005
  }
1261
- const baseEntries = await collectPersistentSnapshotEntries(
1262
- runtimeUserDataDir,
1263
- profileDirectory
1264
- );
1265
1006
  return {
1266
- baseEntries: serializeSnapshotManifestEntries(baseEntries),
1267
- profileDirectory
1007
+ pid,
1008
+ processStartedAtMs
1268
1009
  };
1269
1010
  }
1270
- async function writeRuntimeProfileMetadata(userDataDir, metadata) {
1271
- await (0, import_promises.writeFile)(
1272
- (0, import_node_path.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1273
- JSON.stringify(metadata, null, 2)
1274
- );
1011
+ function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1012
+ return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
1275
1013
  }
1276
- async function readRuntimeProfileMetadata(userDataDir) {
1277
- try {
1278
- const raw = await (0, import_promises.readFile)(
1279
- (0, import_node_path.join)(userDataDir, OPENSTEER_RUNTIME_META_FILE),
1280
- "utf8"
1281
- );
1282
- const parsed = JSON.parse(raw);
1283
- const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
1284
- if (profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
1285
- return null;
1286
- }
1287
- const baseEntries = deserializeSnapshotManifestEntries(
1288
- parsed.baseEntries
1289
- );
1290
- return {
1291
- baseEntries: Object.fromEntries(baseEntries),
1292
- profileDirectory
1293
- };
1294
- } catch {
1014
+ async function readProcessStartedAtMs(pid) {
1015
+ if (pid <= 0) {
1295
1016
  return null;
1296
1017
  }
1297
- }
1298
- async function requireRuntimeProfileMetadata(userDataDir, expectedProfileDirectory) {
1299
- const metadata = await readRuntimeProfileMetadata(userDataDir);
1300
- if (!metadata) {
1301
- throw new Error(
1302
- `Runtime profile metadata was not found for "${userDataDir}".`
1303
- );
1304
- }
1305
- if (metadata.profileDirectory !== expectedProfileDirectory) {
1306
- throw new Error(
1307
- `Runtime profile "${userDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
1308
- );
1309
- }
1310
- return metadata;
1311
- }
1312
- async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
1313
- let rootEntries;
1314
- try {
1315
- rootEntries = await (0, import_promises.readdir)(userDataDir, {
1316
- encoding: "utf8",
1317
- withFileTypes: true
1318
- });
1319
- } catch {
1320
- return /* @__PURE__ */ new Map();
1321
- }
1322
- rootEntries.sort((left, right) => left.name.localeCompare(right.name));
1323
- const collected = /* @__PURE__ */ new Map();
1324
- for (const entry of rootEntries) {
1325
- if (!shouldIncludePersistentRootEntry(
1326
- userDataDir,
1327
- profileDirectory,
1328
- entry.name
1329
- )) {
1330
- continue;
1331
- }
1332
- await collectSnapshotEntry(
1333
- (0, import_node_path.join)(userDataDir, entry.name),
1334
- entry.name,
1335
- collected
1336
- );
1337
- }
1338
- return collected;
1339
- }
1340
- function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
1341
- if (entry === profileDirectory) {
1342
- return true;
1343
- }
1344
- if (COPY_SKIP_ENTRIES.has(entry)) {
1345
- return false;
1018
+ if (process.platform === "linux") {
1019
+ return await readLinuxProcessStartedAtMs(pid);
1346
1020
  }
1347
- if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
1348
- return false;
1021
+ if (process.platform === "win32") {
1022
+ return await readWindowsProcessStartedAtMs(pid);
1349
1023
  }
1350
- return !isProfileDirectory(userDataDir, entry);
1024
+ return await readPsProcessStartedAtMs(pid);
1351
1025
  }
1352
- async function collectSnapshotEntry(sourcePath, relativePath, collected) {
1353
- let entryStat;
1026
+ async function readLinuxProcessStartedAtMs(pid) {
1027
+ let statRaw;
1354
1028
  try {
1355
- entryStat = await (0, import_promises.stat)(sourcePath);
1029
+ statRaw = await (0, import_promises.readFile)(`/proc/${pid}/stat`, "utf8");
1356
1030
  } catch {
1357
- return;
1358
- }
1359
- if (entryStat.isDirectory()) {
1360
- collected.set(relativePath, {
1361
- kind: "directory",
1362
- hash: null,
1363
- sourcePath
1364
- });
1365
- let children;
1366
- try {
1367
- children = await (0, import_promises.readdir)(sourcePath, {
1368
- encoding: "utf8",
1369
- withFileTypes: true
1370
- });
1371
- } catch {
1372
- return;
1373
- }
1374
- children.sort((left, right) => left.name.localeCompare(right.name));
1375
- for (const child of children) {
1376
- await collectSnapshotEntry(
1377
- (0, import_node_path.join)(sourcePath, child.name),
1378
- (0, import_node_path.join)(relativePath, child.name),
1379
- collected
1380
- );
1381
- }
1382
- return;
1383
- }
1384
- if (entryStat.isFile()) {
1385
- collected.set(relativePath, {
1386
- kind: "file",
1387
- hash: await hashFile(sourcePath),
1388
- sourcePath
1389
- });
1031
+ return null;
1390
1032
  }
1391
- }
1392
- function serializeSnapshotManifestEntries(entries) {
1393
- return Object.fromEntries(
1394
- [...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
1395
- relativePath,
1396
- {
1397
- kind: entry.kind,
1398
- hash: entry.hash
1399
- }
1400
- ])
1401
- );
1402
- }
1403
- function deserializeSnapshotManifestEntries(entries) {
1404
- const manifestEntries = /* @__PURE__ */ new Map();
1405
- for (const [relativePath, entry] of Object.entries(entries)) {
1406
- if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
1407
- throw new Error(
1408
- `Runtime profile metadata for "${relativePath}" is invalid.`
1409
- );
1410
- }
1411
- manifestEntries.set(relativePath, {
1412
- kind: entry.kind,
1413
- hash: entry.hash
1414
- });
1033
+ const startTicks = parseLinuxProcessStartTicks(statRaw);
1034
+ if (startTicks === null) {
1035
+ return null;
1415
1036
  }
1416
- return manifestEntries;
1417
- }
1418
- function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
1419
- const mergedEntries = /* @__PURE__ */ new Map();
1420
- const relativePaths = /* @__PURE__ */ new Set([
1421
- ...baseEntries.keys(),
1422
- ...currentEntries.keys(),
1423
- ...runtimeEntries.keys()
1037
+ const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1038
+ readLinuxBootTimeMs(),
1039
+ readLinuxClockTicksPerSecond()
1424
1040
  ]);
1425
- for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
1426
- mergedEntries.set(
1427
- relativePath,
1428
- resolveMergedSnapshotEntrySelection(
1429
- relativePath,
1430
- baseEntries.get(relativePath) ?? null,
1431
- currentEntries.get(relativePath) ?? null,
1432
- runtimeEntries.get(relativePath) ?? null
1433
- )
1434
- );
1435
- }
1436
- return mergedEntries;
1437
- }
1438
- function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
1439
- if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
1440
- return currentEntry ? "current" : null;
1441
- }
1442
- if (snapshotEntriesEqual(currentEntry, baseEntry)) {
1443
- return runtimeEntry ? "runtime" : null;
1444
- }
1445
- if (!baseEntry) {
1446
- if (!currentEntry) {
1447
- return runtimeEntry ? "runtime" : null;
1448
- }
1449
- if (!runtimeEntry) {
1450
- return "current";
1451
- }
1452
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1453
- return "current";
1454
- }
1455
- throw new Error(
1456
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1457
- );
1458
- }
1459
- if (!currentEntry && !runtimeEntry) {
1041
+ if (bootTimeMs === null || clockTicksPerSecond === null) {
1460
1042
  return null;
1461
1043
  }
1462
- if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
1463
- return currentEntry ? "current" : null;
1464
- }
1465
- throw new Error(
1466
- `Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
1044
+ return Math.floor(
1045
+ bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
1467
1046
  );
1468
1047
  }
1469
- function snapshotEntriesEqual(left, right) {
1470
- if (!left || !right) {
1471
- return left === right;
1048
+ function parseLinuxProcessStartTicks(statRaw) {
1049
+ const closingParenIndex = statRaw.lastIndexOf(")");
1050
+ if (closingParenIndex === -1) {
1051
+ return null;
1472
1052
  }
1473
- return left.kind === right.kind && left.hash === right.hash;
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;
1474
1056
  }
1475
- async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
1476
- const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
1477
- ([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
1478
- );
1479
- for (const [relativePath, selection] of selectedEntries) {
1480
- const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
1481
- if (!entry) {
1482
- continue;
1483
- }
1484
- const targetPath = (0, import_node_path.join)(targetUserDataDir, relativePath);
1485
- if (entry.kind === "directory") {
1486
- await (0, import_promises.mkdir)(targetPath, { recursive: true });
1487
- continue;
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;
1488
1063
  }
1489
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(targetPath), { recursive: true });
1490
- await (0, import_promises.copyFile)(entry.sourcePath, targetPath);
1491
- }
1492
- }
1493
- function compareSnapshotPaths(left, right) {
1494
- const leftDepth = left.split(import_node_path.sep).length;
1495
- const rightDepth = right.split(import_node_path.sep).length;
1496
- if (leftDepth !== rightDepth) {
1497
- return leftDepth - rightDepth;
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;
1498
1071
  }
1499
- return left.localeCompare(right);
1500
- }
1501
- async function hashFile(filePath) {
1502
- return new Promise((resolve, reject) => {
1503
- const hash = (0, import_node_crypto.createHash)("sha256");
1504
- const stream = (0, import_node_fs.createReadStream)(filePath);
1505
- stream.on("data", (chunk) => {
1506
- hash.update(chunk);
1507
- });
1508
- stream.on("error", reject);
1509
- stream.on("end", () => {
1510
- resolve(hash.digest("hex"));
1511
- });
1512
- });
1513
1072
  }
1514
- async function ensurePersistentProfileMetadata(userDataDir, metadata) {
1515
- if ((0, import_node_fs.existsSync)((0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE))) {
1516
- return;
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);
1517
1082
  }
1518
- await writePersistentProfileMetadata(userDataDir, metadata);
1083
+ return await linuxClockTicksPerSecondPromise;
1519
1084
  }
1520
- async function readPersistentProfileMetadata(userDataDir) {
1085
+ async function readWindowsProcessStartedAtMs(pid) {
1521
1086
  try {
1522
- const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, OPENSTEER_META_FILE), "utf8");
1523
- const parsed = JSON.parse(raw);
1524
- if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
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
+ }
1098
+ );
1099
+ const isoTimestamp = stdout.trim();
1100
+ if (!isoTimestamp) {
1525
1101
  return null;
1526
1102
  }
1527
- return {
1528
- createdAt: parsed.createdAt,
1529
- profileDirectory: parsed.profileDirectory,
1530
- source: parsed.source
1531
- };
1103
+ const startedAtMs = Date.parse(isoTimestamp);
1104
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1532
1105
  } catch {
1533
1106
  return null;
1534
1107
  }
1535
1108
  }
1536
- async function requirePersistentProfileMetadata(userDataDir) {
1537
- const metadata = await readPersistentProfileMetadata(userDataDir);
1538
- if (!metadata) {
1539
- throw new Error(
1540
- `Persistent profile metadata was not found for "${userDataDir}".`
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
1118
+ }
1541
1119
  );
1120
+ const startedAt = stdout.trim();
1121
+ if (!startedAt) {
1122
+ return null;
1123
+ }
1124
+ const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
1125
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
1126
+ } catch {
1127
+ return null;
1542
1128
  }
1543
- return metadata;
1544
- }
1545
- function wasDirPublishedByAnotherProcess(error, targetDirPath) {
1546
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1547
- return (0, import_node_fs.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1548
1129
  }
1549
- async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
1550
- if (!(0, import_node_fs.existsSync)(targetUserDataDir)) {
1551
- await (0, import_promises.rename)(replacementUserDataDir, targetUserDataDir);
1552
- return;
1553
- }
1554
- const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
1555
- let targetMovedToBackup = false;
1556
- let replacementPublished = false;
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);
1557
1137
  try {
1558
- await (0, import_promises.rename)(targetUserDataDir, backupUserDataDir);
1559
- targetMovedToBackup = true;
1560
- await (0, import_promises.rename)(replacementUserDataDir, targetUserDataDir);
1561
- replacementPublished = true;
1562
- } catch (error) {
1563
- if (targetMovedToBackup && !(0, import_node_fs.existsSync)(targetUserDataDir)) {
1564
- await (0, import_promises.rename)(backupUserDataDir, targetUserDataDir).catch(() => void 0);
1565
- }
1566
- throw error;
1138
+ return await action();
1567
1139
  } finally {
1568
- if (replacementPublished && targetMovedToBackup && (0, import_node_fs.existsSync)(backupUserDataDir)) {
1569
- await (0, import_promises.rm)(backupUserDataDir, {
1570
- recursive: true,
1571
- force: true
1572
- }).catch(() => void 0);
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;
1573
1148
  }
1149
+ await sleep(LOCK_RETRY_DELAY_MS);
1574
1150
  }
1575
1151
  }
1576
- async function withPersistentProfileLock(targetUserDataDir, action) {
1577
- const lockDirPath = buildPersistentProfileLockDirPath(targetUserDataDir);
1578
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(lockDirPath), { recursive: true });
1152
+ async function tryAcquireDirLock(lockDirPath) {
1153
+ await (0, import_promises2.mkdir)((0, import_node_path.dirname)(lockDirPath), { recursive: true });
1579
1154
  while (true) {
1580
- const tempLockDirPath = `${lockDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${(0, import_node_crypto.randomUUID)()}`;
1155
+ const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1581
1156
  try {
1582
- await (0, import_promises.mkdir)(tempLockDirPath);
1583
- await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_LOCK_OWNER);
1157
+ await (0, import_promises2.mkdir)(tempLockDirPath);
1158
+ await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
1584
1159
  try {
1585
- await (0, import_promises.rename)(tempLockDirPath, lockDirPath);
1160
+ await (0, import_promises2.rename)(tempLockDirPath, lockDirPath);
1586
1161
  break;
1587
1162
  } catch (error) {
1588
1163
  if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
@@ -1590,7 +1165,7 @@ async function withPersistentProfileLock(targetUserDataDir, action) {
1590
1165
  }
1591
1166
  }
1592
1167
  } finally {
1593
- await (0, import_promises.rm)(tempLockDirPath, {
1168
+ await (0, import_promises2.rm)(tempLockDirPath, {
1594
1169
  recursive: true,
1595
1170
  force: true
1596
1171
  }).catch(() => void 0);
@@ -1599,52 +1174,48 @@ async function withPersistentProfileLock(targetUserDataDir, action) {
1599
1174
  if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1600
1175
  continue;
1601
1176
  }
1602
- await sleep(PROFILE_LOCK_RETRY_DELAY_MS);
1177
+ return null;
1603
1178
  }
1604
- try {
1605
- return await action();
1606
- } finally {
1607
- await (0, import_promises.rm)(lockDirPath, {
1179
+ return async () => {
1180
+ await (0, import_promises2.rm)(lockDirPath, {
1608
1181
  recursive: true,
1609
1182
  force: true
1610
1183
  }).catch(() => void 0);
1184
+ };
1185
+ }
1186
+ async function isDirLockHeld(lockDirPath) {
1187
+ if (!(0, import_node_fs.existsSync)(lockDirPath)) {
1188
+ return false;
1189
+ }
1190
+ const owner = await readLockOwner(lockDirPath);
1191
+ if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
1192
+ return false;
1611
1193
  }
1194
+ return (0, import_node_fs.existsSync)(lockDirPath);
1195
+ }
1196
+ function getErrorCode(error) {
1197
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1198
+ }
1199
+ function wasDirPublishedByAnotherProcess(error, targetDirPath) {
1200
+ const code = getErrorCode(error);
1201
+ return (0, import_node_fs.existsSync)(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
1612
1202
  }
1613
1203
  async function writeLockOwner(lockDirPath, owner) {
1614
- await writeLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE), owner);
1204
+ await (0, import_promises2.writeFile)((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
1615
1205
  }
1616
1206
  async function readLockOwner(lockDirPath) {
1617
1207
  return await readLockParticipant((0, import_node_path.join)(lockDirPath, LOCK_OWNER_FILE));
1618
1208
  }
1619
- async function writeLockParticipant(filePath, owner, options) {
1620
- await (0, import_promises.writeFile)(filePath, JSON.stringify(owner), options);
1621
- }
1622
1209
  async function readLockParticipant(filePath) {
1623
1210
  return (await readLockParticipantRecord(filePath)).owner;
1624
1211
  }
1625
- async function readLockReclaimerRecord(lockDirPath) {
1626
- return await readLockParticipantRecord(
1627
- (0, import_node_path.join)(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
1628
- );
1629
- }
1630
1212
  async function readLockParticipantRecord(filePath) {
1631
1213
  try {
1632
- const raw = await (0, import_promises.readFile)(filePath, "utf8");
1633
- const parsed = JSON.parse(raw);
1634
- const pid = Number(parsed.pid);
1635
- const processStartedAtMs = Number(parsed.processStartedAtMs);
1636
- if (!Number.isInteger(pid) || !Number.isInteger(processStartedAtMs)) {
1637
- return {
1638
- exists: true,
1639
- owner: null
1640
- };
1641
- }
1214
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
1215
+ const owner = parseProcessOwner(JSON.parse(raw));
1642
1216
  return {
1643
1217
  exists: true,
1644
- owner: {
1645
- pid,
1646
- processStartedAtMs
1647
- }
1218
+ owner
1648
1219
  };
1649
1220
  } catch (error) {
1650
1221
  return {
@@ -1653,6 +1224,11 @@ async function readLockParticipantRecord(filePath) {
1653
1224
  };
1654
1225
  }
1655
1226
  }
1227
+ async function readLockReclaimerRecord(lockDirPath) {
1228
+ return await readLockParticipantRecord(
1229
+ (0, import_node_path.join)(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
1230
+ );
1231
+ }
1656
1232
  async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1657
1233
  if (!await tryAcquireLockReclaimer(lockDirPath)) {
1658
1234
  return false;
@@ -1660,13 +1236,13 @@ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1660
1236
  let reclaimed = false;
1661
1237
  try {
1662
1238
  const owner = await readLockOwner(lockDirPath);
1663
- if (!lockOwnersEqual(owner, expectedOwner)) {
1239
+ if (!processOwnersEqual(owner, expectedOwner)) {
1664
1240
  return false;
1665
1241
  }
1666
1242
  if (owner && await getProcessLiveness(owner) !== "dead") {
1667
1243
  return false;
1668
1244
  }
1669
- await (0, import_promises.rm)(lockDirPath, {
1245
+ await (0, import_promises2.rm)(lockDirPath, {
1670
1246
  recursive: true,
1671
1247
  force: true
1672
1248
  }).catch(() => void 0);
@@ -1674,7 +1250,7 @@ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1674
1250
  return reclaimed;
1675
1251
  } finally {
1676
1252
  if (!reclaimed) {
1677
- await (0, import_promises.rm)(buildLockReclaimerDirPath(lockDirPath), {
1253
+ await (0, import_promises2.rm)(buildLockReclaimerDirPath(lockDirPath), {
1678
1254
  recursive: true,
1679
1255
  force: true
1680
1256
  }).catch(() => void 0);
@@ -1684,12 +1260,12 @@ async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
1684
1260
  async function tryAcquireLockReclaimer(lockDirPath) {
1685
1261
  const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
1686
1262
  while (true) {
1687
- const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${(0, import_node_crypto.randomUUID)()}`;
1263
+ const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${(0, import_node_crypto.randomUUID)()}`;
1688
1264
  try {
1689
- await (0, import_promises.mkdir)(tempReclaimerDirPath);
1690
- await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_LOCK_OWNER);
1265
+ await (0, import_promises2.mkdir)(tempReclaimerDirPath);
1266
+ await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
1691
1267
  try {
1692
- await (0, import_promises.rename)(tempReclaimerDirPath, reclaimerDirPath);
1268
+ await (0, import_promises2.rename)(tempReclaimerDirPath, reclaimerDirPath);
1693
1269
  return true;
1694
1270
  } catch (error) {
1695
1271
  if (getErrorCode(error) === "ENOENT") {
@@ -1706,7 +1282,7 @@ async function tryAcquireLockReclaimer(lockDirPath) {
1706
1282
  }
1707
1283
  throw error;
1708
1284
  } finally {
1709
- await (0, import_promises.rm)(tempReclaimerDirPath, {
1285
+ await (0, import_promises2.rm)(tempReclaimerDirPath, {
1710
1286
  recursive: true,
1711
1287
  force: true
1712
1288
  }).catch(() => void 0);
@@ -1718,256 +1294,1515 @@ async function tryAcquireLockReclaimer(lockDirPath) {
1718
1294
  if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
1719
1295
  return false;
1720
1296
  }
1721
- await (0, import_promises.rm)(reclaimerDirPath, {
1297
+ await (0, import_promises2.rm)(reclaimerDirPath, {
1722
1298
  recursive: true,
1723
1299
  force: true
1724
1300
  }).catch(() => void 0);
1725
1301
  }
1726
1302
  }
1727
- function lockOwnersEqual(left, right) {
1728
- if (!left || !right) {
1729
- return left === right;
1730
- }
1731
- return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
1303
+ function buildLockReclaimerDirPath(lockDirPath) {
1304
+ return (0, import_node_path.join)(lockDirPath, LOCK_RECLAIMER_DIR);
1732
1305
  }
1733
- async function getProcessLiveness(owner) {
1734
- if (owner.pid === process.pid && hasMatchingProcessStartTime(
1735
- owner.processStartedAtMs,
1736
- PROCESS_STARTED_AT_MS
1737
- )) {
1738
- return "live";
1739
- }
1740
- const startedAtMs = await readProcessStartedAtMs(owner.pid);
1741
- if (typeof startedAtMs === "number") {
1742
- return hasMatchingProcessStartTime(
1743
- owner.processStartedAtMs,
1744
- startedAtMs
1745
- ) ? "live" : "dead";
1746
- }
1747
- return isProcessRunning(owner.pid) ? "unknown" : "dead";
1306
+ async function sleep(ms) {
1307
+ await new Promise((resolve) => setTimeout(resolve, ms));
1748
1308
  }
1749
- function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
1750
- return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
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
+ );
1751
1317
  }
1752
- function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
1753
- return (0, import_node_path.join)(
1754
- (0, import_node_path.dirname)(targetUserDataDir),
1755
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
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)
1756
1340
  );
1757
1341
  }
1758
- function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
1759
- return `${(0, import_node_path.basename)(targetUserDataDir)}-tmp-`;
1342
+ function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
1343
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(targetUserDataDir), `${(0, import_node_path2.basename)(targetUserDataDir)}.lock`);
1760
1344
  }
1761
- function buildPersistentProfileBackupDirPath(targetUserDataDir) {
1762
- return (0, import_node_path.join)(
1763
- (0, import_node_path.dirname)(targetUserDataDir),
1764
- `${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-backup-${Date.now()}`
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`
1765
1349
  );
1766
1350
  }
1767
- function buildPersistentProfileLockDirPath(targetUserDataDir) {
1768
- return (0, import_node_path.join)((0, import_node_path.dirname)(targetUserDataDir), `${(0, import_node_path.basename)(targetUserDataDir)}.lock`);
1351
+ async function sleep2(ms) {
1352
+ await new Promise((resolve) => setTimeout(resolve, ms));
1769
1353
  }
1770
- function buildLockReclaimerDirPath(lockDirPath) {
1771
- return (0, import_node_path.join)(lockDirPath, LOCK_RECLAIMER_DIR);
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
+ );
1772
1370
  }
1773
- function buildRuntimeProfileKey(sourceUserDataDir) {
1774
- const hash = (0, import_node_crypto.createHash)("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
1775
- return `${sanitizePathSegment((0, import_node_path.basename)(sourceUserDataDir) || "profile")}-${hash}`;
1371
+ function buildSharedSessionLockPath(persistentUserDataDir) {
1372
+ return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
1776
1373
  }
1777
- function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
1778
- return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
1374
+ function buildSharedSessionClientsDirPath(persistentUserDataDir) {
1375
+ return (0, import_node_path3.join)(
1376
+ buildSharedSessionDirPath(persistentUserDataDir),
1377
+ SHARED_SESSION_CLIENTS_DIR
1378
+ );
1779
1379
  }
1780
- function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
1781
- return (0, import_node_path.join)(
1782
- runtimesRootDir,
1783
- `${buildRuntimeProfileDirNamePrefix(sourceUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
1380
+ function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
1381
+ return (0, import_node_path3.join)(
1382
+ buildSharedSessionClientsDirPath(persistentUserDataDir),
1383
+ `${clientId}.json`
1784
1384
  );
1785
1385
  }
1786
- async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
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
+ }
1787
1450
  let entries;
1788
1451
  try {
1789
- entries = await (0, import_promises.readdir)(rootDir, {
1790
- encoding: "utf8",
1791
- withFileTypes: true
1792
- });
1793
- } catch {
1794
- return;
1452
+ entries = await readDirNames(sessionDirPath);
1453
+ } catch (error) {
1454
+ return getErrorCode2(error) !== "ENOENT";
1795
1455
  }
1796
- await Promise.all(
1797
- entries.map(async (entry) => {
1798
- if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
1799
- return;
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;
1800
1464
  }
1801
- if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
1802
- return;
1465
+ continue;
1466
+ }
1467
+ const owner = parseSharedSessionMetadataTempOwner(entry);
1468
+ if (!owner) {
1469
+ if (isSharedSessionMetadataTempFile(entry)) {
1470
+ continue;
1803
1471
  }
1804
- await (0, import_promises.rm)((0, import_node_path.join)(rootDir, entry.name), {
1805
- recursive: true,
1806
- force: true
1807
- }).catch(() => void 0);
1808
- })
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
1809
1492
  );
1810
1493
  }
1811
- async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
1812
- const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
1813
- return owner ? await getProcessLiveness(owner) !== "dead" : false;
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
+ );
1814
1505
  }
1815
- function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
1816
- const remainder = ownedDirName.slice(ownedDirPrefix.length);
1817
- const firstDashIndex = remainder.indexOf("-");
1818
- const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
1819
- if (firstDashIndex === -1 || secondDashIndex === -1) {
1506
+ function parseSharedSessionMetadata(value) {
1507
+ if (!value || typeof value !== "object") {
1820
1508
  return null;
1821
1509
  }
1822
- const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
1823
- const processStartedAtMs = Number.parseInt(
1824
- remainder.slice(firstDashIndex + 1, secondDashIndex),
1825
- 10
1826
- );
1827
- if (!Number.isInteger(pid) || pid <= 0) {
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) {
1828
1515
  return null;
1829
1516
  }
1830
- if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
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)) {
1831
1532
  return null;
1832
1533
  }
1833
- return { pid, processStartedAtMs };
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
+ });
1834
1542
  }
1835
- function isProcessRunning(pid) {
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) {
1836
1550
  try {
1837
- process.kill(pid, 0);
1838
- return true;
1551
+ return (await readDirNames(dirPath)).length > 0;
1839
1552
  } catch (error) {
1840
- const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1841
- return code !== "ESRCH";
1553
+ return getErrorCode2(error) !== "ENOENT";
1842
1554
  }
1843
1555
  }
1844
- async function readProcessStartedAtMs(pid) {
1845
- if (pid <= 0) {
1846
- return null;
1847
- }
1848
- if (process.platform === "linux") {
1849
- return await readLinuxProcessStartedAtMs(pid);
1850
- }
1851
- if (process.platform === "win32") {
1852
- return await readWindowsProcessStartedAtMs(pid);
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
+ };
1853
1621
  }
1854
- return await readPsProcessStartedAtMs(pid);
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
+ });
1855
1646
  }
1856
- async function readLinuxProcessStartedAtMs(pid) {
1857
- let statRaw;
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;
1858
1675
  try {
1859
- statRaw = await (0, import_promises.readFile)(`/proc/${pid}/stat`, "utf8");
1860
- } catch (error) {
1861
- return null;
1862
- }
1863
- const startTicks = parseLinuxProcessStartTicks(statRaw);
1864
- if (startTicks === null) {
1865
- return null;
1676
+ entries = await (0, import_promises4.readdir)(sourceUserDataDir);
1677
+ } catch {
1678
+ return;
1866
1679
  }
1867
- const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
1868
- readLinuxBootTimeMs(),
1869
- readLinuxClockTicksPerSecond()
1870
- ]);
1871
- if (bootTimeMs === null || clockTicksPerSecond === null) {
1872
- return null;
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
+ }
1873
1704
  }
1874
- return Math.floor(
1875
- bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
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)
1876
1711
  );
1877
1712
  }
1878
- function parseLinuxProcessStartTicks(statRaw) {
1879
- const closingParenIndex = statRaw.lastIndexOf(")");
1880
- if (closingParenIndex === -1) {
1881
- return null;
1882
- }
1883
- const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
1884
- const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
1885
- return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
1713
+ function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
1714
+ return {
1715
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1716
+ profileDirectory,
1717
+ source: sourceUserDataDir
1718
+ };
1886
1719
  }
1887
- async function readLinuxBootTimeMs() {
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;
1888
1728
  try {
1889
- const statRaw = await (0, import_promises.readFile)("/proc/stat", "utf8");
1890
- const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
1891
- if (!bootTimeLine) {
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") {
1892
1832
  return null;
1893
1833
  }
1894
- const bootTimeSeconds = Number.parseInt(
1895
- bootTimeLine.slice("btime ".length),
1896
- 10
1897
- );
1898
- return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
1899
- } catch {
1900
- return null;
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.";
2736
+ }
2737
+ } catch (error) {
2738
+ lastError = error instanceof Error ? error.message : "Unknown error";
2739
+ }
2740
+ await sleep5(100);
1901
2741
  }
1902
- }
1903
- async function readLinuxClockTicksPerSecond() {
1904
- linuxClockTicksPerSecondPromise ??= execFileAsync(
1905
- "getconf",
1906
- ["CLK_TCK"]
1907
- ).then(
1908
- ({ stdout }) => {
1909
- const value = Number.parseInt(stdout.trim(), 10);
1910
- return Number.isFinite(value) && value > 0 ? value : null;
1911
- },
1912
- () => null
2742
+ throw new Error(
2743
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1913
2744
  );
1914
- return await linuxClockTicksPerSecondPromise;
1915
2745
  }
1916
- async function readPsProcessStartedAtMs(pid) {
2746
+ function normalizeDiscoveryUrl(cdpUrl) {
2747
+ let parsed;
1917
2748
  try {
1918
- const { stdout } = await execFileAsync(
1919
- "ps",
1920
- ["-p", String(pid), "-o", "lstart="],
1921
- { env: PS_COMMAND_ENV }
2749
+ parsed = new URL(cdpUrl);
2750
+ } catch {
2751
+ throw new Error(
2752
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1922
2753
  );
1923
- return parsePsStartedAtMs(stdout);
1924
- } catch (error) {
1925
- return null;
1926
2754
  }
1927
- }
1928
- function parsePsStartedAtMs(stdout) {
1929
- const raw = stdout.trim();
1930
- if (!raw) {
1931
- return null;
2755
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2756
+ return parsed;
1932
2757
  }
1933
- const startedAtMs = Date.parse(raw);
1934
- return Number.isNaN(startedAtMs) ? null : startedAtMs;
1935
- }
1936
- async function readWindowsProcessStartedAtMs(pid) {
1937
- const script = [
1938
- "$process = Get-Process -Id " + String(pid) + " -ErrorAction SilentlyContinue",
1939
- "if ($null -eq $process) { exit 3 }",
1940
- '$process.StartTime.ToUniversalTime().ToString("o")'
1941
- ].join("; ");
1942
- try {
1943
- const { stdout } = await execFileAsync(
1944
- "powershell.exe",
1945
- ["-NoLogo", "-NoProfile", "-Command", script]
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).`
1946
2761
  );
1947
- return parsePsStartedAtMs(stdout);
1948
- } catch (error) {
1949
- return null;
1950
2762
  }
2763
+ const normalized = new URL(parsed.toString());
2764
+ normalized.pathname = "/json/version";
2765
+ normalized.search = "";
2766
+ normalized.hash = "";
2767
+ return normalized;
1951
2768
  }
1952
- function getErrorCode(error) {
1953
- return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
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
+ });
1954
2790
  }
1955
- async function sleep(ms) {
2791
+ async function sleep5(ms) {
1956
2792
  await new Promise((resolve) => setTimeout(resolve, ms));
1957
2793
  }
1958
2794
 
1959
2795
  // src/browser/pool.ts
1960
2796
  var BrowserPool = class {
1961
2797
  browser = null;
1962
- cdpProxy = null;
1963
- launchedProcess = null;
1964
- managedRuntimeProfile = null;
2798
+ activeSessionClose = null;
2799
+ closeInFlight = null;
1965
2800
  defaults;
1966
2801
  constructor(defaults = {}) {
1967
2802
  this.defaults = defaults;
1968
2803
  }
1969
2804
  async launch(options = {}) {
1970
- if (this.browser || this.cdpProxy || this.launchedProcess || this.managedRuntimeProfile) {
2805
+ if (this.browser || this.activeSessionClose) {
1971
2806
  await this.close();
1972
2807
  }
1973
2808
  const mode = options.mode ?? this.defaults.mode ?? "chromium";
@@ -2014,31 +2849,27 @@ var BrowserPool = class {
2014
2849
  return this.launchSandbox(options);
2015
2850
  }
2016
2851
  async close() {
2017
- const browser = this.browser;
2018
- const cdpProxy = this.cdpProxy;
2019
- const launchedProcess = this.launchedProcess;
2020
- const managedRuntimeProfile = this.managedRuntimeProfile;
2021
- this.browser = null;
2022
- this.cdpProxy = null;
2023
- this.launchedProcess = null;
2852
+ if (this.closeInFlight) {
2853
+ await this.closeInFlight;
2854
+ return;
2855
+ }
2856
+ const closeOperation = this.closeCurrent();
2857
+ this.closeInFlight = closeOperation;
2024
2858
  try {
2025
- if (browser) {
2026
- await browser.close().catch(() => void 0);
2027
- }
2859
+ await closeOperation;
2860
+ this.browser = null;
2861
+ this.activeSessionClose = null;
2028
2862
  } finally {
2029
- cdpProxy?.close();
2030
- await killProcessTree(launchedProcess);
2031
- if (managedRuntimeProfile) {
2032
- await persistIsolatedRuntimeProfile(
2033
- managedRuntimeProfile.userDataDir,
2034
- managedRuntimeProfile.persistentUserDataDir
2035
- );
2036
- if (this.managedRuntimeProfile === managedRuntimeProfile) {
2037
- this.managedRuntimeProfile = null;
2038
- }
2039
- }
2863
+ this.closeInFlight = null;
2040
2864
  }
2041
2865
  }
2866
+ async closeCurrent() {
2867
+ if (this.activeSessionClose) {
2868
+ await this.activeSessionClose();
2869
+ return;
2870
+ }
2871
+ await this.browser?.close().catch(() => void 0);
2872
+ }
2042
2873
  async connectToRunning(cdpUrl, timeout) {
2043
2874
  let browser = null;
2044
2875
  let cdpProxy = null;
@@ -2052,11 +2883,14 @@ var BrowserPool = class {
2052
2883
  }
2053
2884
  cdpProxy = new CDPProxy(browserWsUrl, targetId);
2054
2885
  const proxyWsUrl = await cdpProxy.start();
2055
- browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
2886
+ browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
2056
2887
  timeout: timeout ?? 3e4
2057
2888
  });
2058
2889
  this.browser = browser;
2059
- this.cdpProxy = cdpProxy;
2890
+ this.activeSessionClose = async () => {
2891
+ await browser?.close().catch(() => void 0);
2892
+ cdpProxy?.close();
2893
+ };
2060
2894
  const { context, page } = await pickBrowserContextAndPage(browser);
2061
2895
  return { browser, context, page, isExternal: true };
2062
2896
  } catch (error) {
@@ -2065,7 +2899,7 @@ var BrowserPool = class {
2065
2899
  }
2066
2900
  cdpProxy?.close();
2067
2901
  this.browser = null;
2068
- this.cdpProxy = null;
2902
+ this.activeSessionClose = null;
2069
2903
  throw error;
2070
2904
  }
2071
2905
  }
@@ -2085,60 +2919,29 @@ var BrowserPool = class {
2085
2919
  sourceUserDataDir,
2086
2920
  profileDirectory
2087
2921
  );
2088
- const runtimeProfile = await createIsolatedRuntimeProfile(
2089
- persistentProfile.userDataDir
2090
- );
2091
- const debugPort = await reserveDebugPort();
2092
- const headless = resolveLaunchHeadless(
2093
- "real",
2094
- options.headless,
2095
- this.defaults.headless
2096
- );
2097
- const launchArgs = buildRealBrowserLaunchArgs({
2098
- userDataDir: runtimeProfile.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,
2099
2931
  profileDirectory,
2100
- debugPort,
2101
- headless
2102
- });
2103
- const processHandle = (0, import_node_child_process2.spawn)(executablePath, launchArgs, {
2104
- detached: process.platform !== "win32",
2105
- stdio: "ignore"
2932
+ timeoutMs: options.timeout ?? 3e4
2106
2933
  });
2107
- processHandle.unref();
2108
- let browser = null;
2109
- try {
2110
- const wsUrl = await resolveCdpWebSocketUrl(
2111
- `http://127.0.0.1:${debugPort}`,
2112
- options.timeout ?? 3e4
2113
- );
2114
- browser = await import_playwright.chromium.connectOverCDP(wsUrl, {
2115
- timeout: options.timeout ?? 3e4
2116
- });
2117
- const { context, page } = await createOwnedBrowserContextAndPage(
2118
- browser
2119
- );
2120
- if (options.initialUrl) {
2121
- await page.goto(options.initialUrl, {
2122
- waitUntil: "domcontentloaded",
2123
- timeout: options.timeout ?? 3e4
2124
- });
2125
- }
2126
- this.browser = browser;
2127
- this.launchedProcess = processHandle;
2128
- this.managedRuntimeProfile = runtimeProfile;
2129
- return { browser, context, page, isExternal: false };
2130
- } catch (error) {
2131
- await browser?.close().catch(() => void 0);
2132
- await killProcessTree(processHandle);
2133
- await (0, import_promises2.rm)(runtimeProfile.userDataDir, {
2134
- recursive: true,
2135
- force: true
2136
- }).catch(() => void 0);
2137
- throw error;
2138
- }
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
+ };
2139
2942
  }
2140
2943
  async launchSandbox(options) {
2141
- const browser = await import_playwright.chromium.launch({
2944
+ const browser = await import_playwright2.chromium.launch({
2142
2945
  headless: resolveLaunchHeadless(
2143
2946
  "chromium",
2144
2947
  options.headless,
@@ -2151,11 +2954,14 @@ var BrowserPool = class {
2151
2954
  const context = await browser.newContext(options.context || {});
2152
2955
  const page = await context.newPage();
2153
2956
  this.browser = browser;
2957
+ this.activeSessionClose = async () => {
2958
+ await browser.close().catch(() => void 0);
2959
+ };
2154
2960
  return { browser, context, page, isExternal: false };
2155
2961
  }
2156
2962
  };
2157
2963
  async function pickBrowserContextAndPage(browser) {
2158
- const context = getPrimaryBrowserContext(browser);
2964
+ const context = getPrimaryBrowserContext2(browser);
2159
2965
  const page = await getAttachedPageOrCreate(context);
2160
2966
  return { context, page };
2161
2967
  }
@@ -2168,11 +2974,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
2168
2974
  }
2169
2975
  return mode === "real";
2170
2976
  }
2171
- async function createOwnedBrowserContextAndPage(browser) {
2172
- const context = getPrimaryBrowserContext(browser);
2173
- const page = await getExistingPageOrCreate(context);
2174
- return { context, page };
2175
- }
2176
2977
  async function getAttachedPageOrCreate(context) {
2177
2978
  const pages = context.pages();
2178
2979
  const inspectablePage = pages.find(
@@ -2187,14 +2988,7 @@ async function getAttachedPageOrCreate(context) {
2187
2988
  }
2188
2989
  return await context.newPage();
2189
2990
  }
2190
- async function getExistingPageOrCreate(context) {
2191
- const existingPage = context.pages()[0];
2192
- if (existingPage) {
2193
- return existingPage;
2194
- }
2195
- return await context.newPage();
2196
- }
2197
- function getPrimaryBrowserContext(browser) {
2991
+ function getPrimaryBrowserContext2(browser) {
2198
2992
  const contexts = browser.contexts();
2199
2993
  if (contexts.length === 0) {
2200
2994
  throw new Error(
@@ -2213,125 +3007,6 @@ function safePageUrl(page) {
2213
3007
  return "";
2214
3008
  }
2215
3009
  }
2216
- function normalizeDiscoveryUrl(cdpUrl) {
2217
- let parsed;
2218
- try {
2219
- parsed = new URL(cdpUrl);
2220
- } catch {
2221
- throw new Error(
2222
- `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
2223
- );
2224
- }
2225
- if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2226
- return parsed;
2227
- }
2228
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2229
- throw new Error(
2230
- `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
2231
- );
2232
- }
2233
- const normalized = new URL(parsed.toString());
2234
- normalized.pathname = "/json/version";
2235
- normalized.search = "";
2236
- normalized.hash = "";
2237
- return normalized;
2238
- }
2239
- async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2240
- if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2241
- return cdpUrl;
2242
- }
2243
- const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2244
- const deadline = Date.now() + timeoutMs;
2245
- let lastError = "CDP discovery did not respond.";
2246
- while (Date.now() < deadline) {
2247
- const remaining = Math.max(deadline - Date.now(), 1e3);
2248
- try {
2249
- const response = await fetch(versionUrl, {
2250
- signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2251
- });
2252
- if (!response.ok) {
2253
- lastError = `${response.status} ${response.statusText}`;
2254
- } else {
2255
- const payload = await response.json();
2256
- const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2257
- if (wsUrl && wsUrl.trim()) {
2258
- return wsUrl;
2259
- }
2260
- lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
2261
- }
2262
- } catch (error) {
2263
- lastError = error instanceof Error ? error.message : "Unknown error";
2264
- }
2265
- await sleep2(100);
2266
- }
2267
- throw new Error(
2268
- `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2269
- );
2270
- }
2271
- async function reserveDebugPort() {
2272
- return await new Promise((resolve, reject) => {
2273
- const server2 = (0, import_node_net.createServer)();
2274
- server2.unref();
2275
- server2.on("error", reject);
2276
- server2.listen(0, "127.0.0.1", () => {
2277
- const address = server2.address();
2278
- if (!address || typeof address === "string") {
2279
- server2.close();
2280
- reject(new Error("Failed to reserve a local debug port."));
2281
- return;
2282
- }
2283
- server2.close((error) => {
2284
- if (error) {
2285
- reject(error);
2286
- return;
2287
- }
2288
- resolve(address.port);
2289
- });
2290
- });
2291
- });
2292
- }
2293
- function buildRealBrowserLaunchArgs(options) {
2294
- const args = [
2295
- `--user-data-dir=${options.userDataDir}`,
2296
- `--profile-directory=${options.profileDirectory}`,
2297
- `--remote-debugging-port=${options.debugPort}`,
2298
- "--disable-blink-features=AutomationControlled"
2299
- ];
2300
- if (options.headless) {
2301
- args.push("--headless=new");
2302
- }
2303
- return args;
2304
- }
2305
- async function killProcessTree(processHandle) {
2306
- if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
2307
- return;
2308
- }
2309
- if (process.platform === "win32") {
2310
- await new Promise((resolve) => {
2311
- const killer = (0, import_node_child_process2.spawn)(
2312
- "taskkill",
2313
- ["/pid", String(processHandle.pid), "/t", "/f"],
2314
- {
2315
- stdio: "ignore"
2316
- }
2317
- );
2318
- killer.on("error", () => resolve());
2319
- killer.on("exit", () => resolve());
2320
- });
2321
- return;
2322
- }
2323
- try {
2324
- process.kill(-processHandle.pid, "SIGKILL");
2325
- } catch {
2326
- try {
2327
- processHandle.kill("SIGKILL");
2328
- } catch {
2329
- }
2330
- }
2331
- }
2332
- async function sleep2(ms) {
2333
- await new Promise((resolve) => setTimeout(resolve, ms));
2334
- }
2335
3010
 
2336
3011
  // src/config.ts
2337
3012
  var import_fs2 = __toESM(require("fs"), 1);
@@ -3603,7 +4278,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
3603
4278
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
3604
4279
  Math.max(0, deadline - Date.now())
3605
4280
  );
3606
- await sleep3(retryDelay);
4281
+ await sleep6(retryDelay);
3607
4282
  }
3608
4283
  }
3609
4284
  }
@@ -3636,7 +4311,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
3636
4311
  () => ({ kind: "resolved" }),
3637
4312
  (error) => ({ kind: "rejected", error })
3638
4313
  );
3639
- const timeoutPromise = sleep3(
4314
+ const timeoutPromise = sleep6(
3640
4315
  timeout + FRAME_EVALUATE_GRACE_MS
3641
4316
  ).then(() => ({ kind: "timeout" }));
3642
4317
  const result = await Promise.race([
@@ -3778,7 +4453,7 @@ function isIgnorableFrameError(error) {
3778
4453
  const message = error.message;
3779
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");
3780
4455
  }
3781
- function sleep3(ms) {
4456
+ function sleep6(ms) {
3782
4457
  return new Promise((resolve) => {
3783
4458
  setTimeout(resolve, ms);
3784
4459
  });
@@ -8029,7 +8704,7 @@ async function closeTab(context, activePage, index) {
8029
8704
  }
8030
8705
 
8031
8706
  // src/actions/cookies.ts
8032
- var import_promises3 = require("fs/promises");
8707
+ var import_promises6 = require("fs/promises");
8033
8708
  async function getCookies(context, url) {
8034
8709
  return context.cookies(url ? [url] : void 0);
8035
8710
  }
@@ -8041,10 +8716,10 @@ async function clearCookies(context) {
8041
8716
  }
8042
8717
  async function exportCookies(context, filePath, url) {
8043
8718
  const cookies = await context.cookies(url ? [url] : void 0);
8044
- 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");
8045
8720
  }
8046
8721
  async function importCookies(context, filePath) {
8047
- const raw = await (0, import_promises3.readFile)(filePath, "utf-8");
8722
+ const raw = await (0, import_promises6.readFile)(filePath, "utf-8");
8048
8723
  const cookies = JSON.parse(raw);
8049
8724
  await context.addCookies(cookies);
8050
8725
  }
@@ -8355,7 +9030,7 @@ var AdaptiveNetworkTracker = class {
8355
9030
  this.idleSince = 0;
8356
9031
  }
8357
9032
  const remaining = Math.max(1, options.deadline - now);
8358
- await sleep4(Math.min(NETWORK_POLL_MS, remaining));
9033
+ await sleep7(Math.min(NETWORK_POLL_MS, remaining));
8359
9034
  }
8360
9035
  }
8361
9036
  handleRequestStarted = (request) => {
@@ -8400,7 +9075,7 @@ var AdaptiveNetworkTracker = class {
8400
9075
  return false;
8401
9076
  }
8402
9077
  };
8403
- async function sleep4(ms) {
9078
+ async function sleep7(ms) {
8404
9079
  await new Promise((resolve) => {
8405
9080
  setTimeout(resolve, ms);
8406
9081
  });
@@ -10102,13 +10777,13 @@ function dedupeNewest(entries) {
10102
10777
  }
10103
10778
 
10104
10779
  // src/cloud/cdp-client.ts
10105
- var import_playwright2 = require("playwright");
10780
+ var import_playwright3 = require("playwright");
10106
10781
  var CloudCdpClient = class {
10107
10782
  async connect(args) {
10108
10783
  const endpoint = withTokenQuery(args.wsUrl, args.token);
10109
10784
  let browser;
10110
10785
  try {
10111
- browser = await import_playwright2.chromium.connectOverCDP(endpoint);
10786
+ browser = await import_playwright3.chromium.connectOverCDP(endpoint);
10112
10787
  } catch (error) {
10113
10788
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
10114
10789
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -11925,7 +12600,7 @@ async function executeAgentAction(page, action) {
11925
12600
  }
11926
12601
  case "wait": {
11927
12602
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
11928
- await sleep5(ms);
12603
+ await sleep8(ms);
11929
12604
  return;
11930
12605
  }
11931
12606
  case "goto": {
@@ -12090,7 +12765,7 @@ async function pressKeyCombo(page, combo) {
12090
12765
  }
12091
12766
  }
12092
12767
  }
12093
- function sleep5(ms) {
12768
+ function sleep8(ms) {
12094
12769
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
12095
12770
  }
12096
12771
 
@@ -12121,7 +12796,7 @@ var OpensteerCuaAgentHandler = class {
12121
12796
  if (isMutatingAgentAction(action)) {
12122
12797
  this.onMutatingAction?.(action);
12123
12798
  }
12124
- await sleep6(this.config.waitBetweenActionsMs);
12799
+ await sleep9(this.config.waitBetweenActionsMs);
12125
12800
  });
12126
12801
  try {
12127
12802
  const result = await this.client.execute({
@@ -12183,7 +12858,7 @@ var OpensteerCuaAgentHandler = class {
12183
12858
  await this.cursorController.preview({ x, y }, "agent");
12184
12859
  }
12185
12860
  };
12186
- function sleep6(ms) {
12861
+ function sleep9(ms) {
12187
12862
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
12188
12863
  }
12189
12864
 
@@ -12619,7 +13294,7 @@ var CursorController = class {
12619
13294
  for (const step of motion.points) {
12620
13295
  await this.renderer.move(step, this.style);
12621
13296
  if (motion.stepDelayMs > 0) {
12622
- await sleep7(motion.stepDelayMs);
13297
+ await sleep10(motion.stepDelayMs);
12623
13298
  }
12624
13299
  }
12625
13300
  if (shouldPulse(intent)) {
@@ -12777,7 +13452,7 @@ function clamp2(value, min, max) {
12777
13452
  function shouldPulse(intent) {
12778
13453
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
12779
13454
  }
12780
- function sleep7(ms) {
13455
+ function sleep10(ms) {
12781
13456
  return new Promise((resolve) => setTimeout(resolve, ms));
12782
13457
  }
12783
13458
 
@@ -13251,15 +13926,19 @@ var Opensteer = class _Opensteer {
13251
13926
  }
13252
13927
  return;
13253
13928
  }
13929
+ let closedOwnedBrowser = false;
13254
13930
  try {
13255
13931
  if (this.ownsBrowser) {
13256
13932
  await this.pool.close();
13933
+ closedOwnedBrowser = true;
13257
13934
  }
13258
13935
  } finally {
13259
13936
  this.browser = null;
13260
13937
  this.pageRef = null;
13261
13938
  this.contextRef = null;
13262
- this.ownsBrowser = false;
13939
+ if (!this.ownsBrowser || closedOwnedBrowser) {
13940
+ this.ownsBrowser = false;
13941
+ }
13263
13942
  if (this.cursorController) {
13264
13943
  await this.cursorController.dispose().catch(() => void 0);
13265
13944
  }
@@ -15606,7 +16285,7 @@ function getMetadataPath(session2) {
15606
16285
  }
15607
16286
 
15608
16287
  // src/cli/commands.ts
15609
- var import_promises4 = require("fs/promises");
16288
+ var import_promises7 = require("fs/promises");
15610
16289
  var commands = {
15611
16290
  async navigate(opensteer, args) {
15612
16291
  const url = args.url;
@@ -15660,7 +16339,7 @@ var commands = {
15660
16339
  fullPage: args.fullPage,
15661
16340
  type
15662
16341
  });
15663
- await (0, import_promises4.writeFile)(file, buffer);
16342
+ await (0, import_promises7.writeFile)(file, buffer);
15664
16343
  return { file };
15665
16344
  },
15666
16345
  async click(opensteer, args) {