opensteer 0.6.11 → 0.6.12
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.
- package/dist/{chunk-JZF2WC7U.js → chunk-RC4IPQDZ.js} +888 -94
- package/dist/cli/profile.cjs +897 -116
- package/dist/cli/profile.js +1 -1
- package/dist/cli/server.cjs +882 -101
- package/dist/cli/server.js +1 -1
- package/dist/index.cjs +891 -96
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/package.json +1 -1
|
@@ -25,24 +25,34 @@ import {
|
|
|
25
25
|
} from "./chunk-3H5RRIMZ.js";
|
|
26
26
|
|
|
27
27
|
// src/browser/persistent-profile.ts
|
|
28
|
-
import { createHash } from "crypto";
|
|
29
|
-
import {
|
|
28
|
+
import { createHash, randomUUID } from "crypto";
|
|
29
|
+
import { execFile } from "child_process";
|
|
30
|
+
import { createReadStream, existsSync } from "fs";
|
|
30
31
|
import {
|
|
31
32
|
cp,
|
|
32
33
|
copyFile,
|
|
33
34
|
mkdir,
|
|
34
35
|
mkdtemp,
|
|
35
36
|
readdir,
|
|
37
|
+
readFile,
|
|
36
38
|
rename,
|
|
37
39
|
rm,
|
|
38
40
|
stat,
|
|
39
41
|
writeFile
|
|
40
42
|
} from "fs/promises";
|
|
41
|
-
import { homedir } from "os";
|
|
42
|
-
import { basename, dirname, join } from "path";
|
|
43
|
+
import { homedir, tmpdir } from "os";
|
|
44
|
+
import { basename, dirname, join, relative, sep } from "path";
|
|
45
|
+
import { promisify } from "util";
|
|
46
|
+
var execFileAsync = promisify(execFile);
|
|
43
47
|
var OPENSTEER_META_FILE = ".opensteer-meta.json";
|
|
48
|
+
var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
|
|
49
|
+
var LOCK_OWNER_FILE = "owner.json";
|
|
50
|
+
var LOCK_RECLAIMER_DIR = "reclaimer";
|
|
44
51
|
var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
|
|
45
52
|
var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
|
|
53
|
+
var PROFILE_LOCK_RETRY_DELAY_MS = 50;
|
|
54
|
+
var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
|
|
55
|
+
var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
|
|
46
56
|
var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
|
|
47
57
|
"SingletonCookie",
|
|
48
58
|
"SingletonLock",
|
|
@@ -52,7 +62,8 @@ var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
|
|
|
52
62
|
]);
|
|
53
63
|
var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
|
|
54
64
|
...CHROME_SINGLETON_ENTRIES,
|
|
55
|
-
OPENSTEER_META_FILE
|
|
65
|
+
OPENSTEER_META_FILE,
|
|
66
|
+
OPENSTEER_RUNTIME_META_FILE
|
|
56
67
|
]);
|
|
57
68
|
var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
58
69
|
"Crash Reports",
|
|
@@ -71,6 +82,11 @@ var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
|
71
82
|
"WidevineCdm",
|
|
72
83
|
"pnacl"
|
|
73
84
|
]);
|
|
85
|
+
var CURRENT_PROCESS_LOCK_OWNER = {
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
processStartedAtMs: PROCESS_STARTED_AT_MS
|
|
88
|
+
};
|
|
89
|
+
var linuxClockTicksPerSecondPromise = null;
|
|
74
90
|
async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
|
|
75
91
|
const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
|
|
76
92
|
const targetUserDataDir = join(
|
|
@@ -83,27 +99,29 @@ async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory,
|
|
|
83
99
|
profileDirectory
|
|
84
100
|
);
|
|
85
101
|
await mkdir(dirname(targetUserDataDir), { recursive: true });
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!existsSync(sourceProfileDir)) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
|
|
102
|
+
return await withPersistentProfileLock(targetUserDataDir, async () => {
|
|
103
|
+
await cleanOrphanedOwnedDirs(
|
|
104
|
+
dirname(targetUserDataDir),
|
|
105
|
+
buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
|
|
93
106
|
);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
if (!existsSync(sourceProfileDir)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const created = await createPersistentProfileClone(
|
|
113
|
+
resolvedSourceUserDataDir,
|
|
114
|
+
sourceProfileDir,
|
|
115
|
+
targetUserDataDir,
|
|
116
|
+
profileDirectory,
|
|
117
|
+
metadata
|
|
118
|
+
);
|
|
119
|
+
await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
|
|
120
|
+
return {
|
|
121
|
+
created,
|
|
122
|
+
userDataDir: targetUserDataDir
|
|
123
|
+
};
|
|
124
|
+
});
|
|
107
125
|
}
|
|
108
126
|
async function clearPersistentProfileSingletons(userDataDir) {
|
|
109
127
|
await Promise.all(
|
|
@@ -115,6 +133,80 @@ async function clearPersistentProfileSingletons(userDataDir) {
|
|
|
115
133
|
)
|
|
116
134
|
);
|
|
117
135
|
}
|
|
136
|
+
async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
|
|
137
|
+
const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
|
|
138
|
+
const runtimeRootDir = expandHome(runtimesRootDir);
|
|
139
|
+
await mkdir(runtimeRootDir, { recursive: true });
|
|
140
|
+
return await withPersistentProfileLock(
|
|
141
|
+
resolvedSourceUserDataDir,
|
|
142
|
+
async () => {
|
|
143
|
+
const sourceMetadata = await readPersistentProfileMetadata(
|
|
144
|
+
resolvedSourceUserDataDir
|
|
145
|
+
);
|
|
146
|
+
await cleanOrphanedOwnedDirs(
|
|
147
|
+
runtimeRootDir,
|
|
148
|
+
buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
|
|
149
|
+
);
|
|
150
|
+
const runtimeUserDataDir = await mkdtemp(
|
|
151
|
+
buildRuntimeProfileDirPrefix(
|
|
152
|
+
runtimeRootDir,
|
|
153
|
+
resolvedSourceUserDataDir
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
try {
|
|
157
|
+
await copyUserDataDirSnapshot(
|
|
158
|
+
resolvedSourceUserDataDir,
|
|
159
|
+
runtimeUserDataDir
|
|
160
|
+
);
|
|
161
|
+
await writeRuntimeProfileMetadata(
|
|
162
|
+
runtimeUserDataDir,
|
|
163
|
+
await buildRuntimeProfileMetadata(
|
|
164
|
+
runtimeUserDataDir,
|
|
165
|
+
sourceMetadata?.profileDirectory ?? null
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
return {
|
|
169
|
+
persistentUserDataDir: resolvedSourceUserDataDir,
|
|
170
|
+
userDataDir: runtimeUserDataDir
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await rm(runtimeUserDataDir, {
|
|
174
|
+
recursive: true,
|
|
175
|
+
force: true
|
|
176
|
+
}).catch(() => void 0);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
|
|
183
|
+
const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
|
|
184
|
+
const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
|
|
185
|
+
await withPersistentProfileLock(resolvedPersistentUserDataDir, async () => {
|
|
186
|
+
await mkdir(dirname(resolvedPersistentUserDataDir), { recursive: true });
|
|
187
|
+
await cleanOrphanedOwnedDirs(
|
|
188
|
+
dirname(resolvedPersistentUserDataDir),
|
|
189
|
+
buildPersistentProfileTempDirNamePrefix(resolvedPersistentUserDataDir)
|
|
190
|
+
);
|
|
191
|
+
const metadata = await requirePersistentProfileMetadata(
|
|
192
|
+
resolvedPersistentUserDataDir
|
|
193
|
+
);
|
|
194
|
+
const runtimeMetadata = await requireRuntimeProfileMetadata(
|
|
195
|
+
resolvedRuntimeUserDataDir,
|
|
196
|
+
metadata.profileDirectory
|
|
197
|
+
);
|
|
198
|
+
await mergePersistentProfileSnapshot(
|
|
199
|
+
resolvedRuntimeUserDataDir,
|
|
200
|
+
resolvedPersistentUserDataDir,
|
|
201
|
+
metadata,
|
|
202
|
+
runtimeMetadata
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
await rm(resolvedRuntimeUserDataDir, {
|
|
206
|
+
recursive: true,
|
|
207
|
+
force: true
|
|
208
|
+
});
|
|
209
|
+
}
|
|
118
210
|
function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
|
|
119
211
|
const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
|
|
120
212
|
const sourceLabel = sanitizePathSegment(basename(sourceUserDataDir) || "user-data");
|
|
@@ -124,6 +216,9 @@ function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
|
|
|
124
216
|
function defaultPersistentProfilesRootDir() {
|
|
125
217
|
return join(homedir(), ".opensteer", "real-browser-profiles");
|
|
126
218
|
}
|
|
219
|
+
function defaultRuntimeProfilesRootDir() {
|
|
220
|
+
return join(tmpdir(), "opensteer-real-browser-runtimes");
|
|
221
|
+
}
|
|
127
222
|
function sanitizePathSegment(value) {
|
|
128
223
|
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
|
|
129
224
|
return sanitized.replace(/^-|-$/g, "") || "profile";
|
|
@@ -131,6 +226,23 @@ function sanitizePathSegment(value) {
|
|
|
131
226
|
function isProfileDirectory(userDataDir, entry) {
|
|
132
227
|
return existsSync(join(userDataDir, entry, "Preferences"));
|
|
133
228
|
}
|
|
229
|
+
async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
|
|
230
|
+
await cp(sourceUserDataDir, targetUserDataDir, {
|
|
231
|
+
recursive: true,
|
|
232
|
+
filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
|
|
236
|
+
const candidateRelativePath = relative(userDataDir, candidatePath);
|
|
237
|
+
if (!candidateRelativePath) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const segments = candidateRelativePath.split(sep).filter(Boolean);
|
|
241
|
+
if (segments.length !== 1) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return !COPY_SKIP_ENTRIES.has(segments[0]);
|
|
245
|
+
}
|
|
134
246
|
async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
|
|
135
247
|
let entries;
|
|
136
248
|
try {
|
|
@@ -187,19 +299,17 @@ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir,
|
|
|
187
299
|
);
|
|
188
300
|
let published = false;
|
|
189
301
|
try {
|
|
190
|
-
await
|
|
191
|
-
recursive: true
|
|
192
|
-
});
|
|
193
|
-
await copyRootLevelEntries(
|
|
302
|
+
await materializePersistentProfileSnapshot(
|
|
194
303
|
sourceUserDataDir,
|
|
304
|
+
sourceProfileDir,
|
|
195
305
|
tempUserDataDir,
|
|
196
|
-
profileDirectory
|
|
306
|
+
profileDirectory,
|
|
307
|
+
metadata
|
|
197
308
|
);
|
|
198
|
-
await writePersistentProfileMetadata(tempUserDataDir, metadata);
|
|
199
309
|
try {
|
|
200
310
|
await rename(tempUserDataDir, targetUserDataDir);
|
|
201
311
|
} catch (error) {
|
|
202
|
-
if (
|
|
312
|
+
if (wasDirPublishedByAnotherProcess(error, targetUserDataDir)) {
|
|
203
313
|
return false;
|
|
204
314
|
}
|
|
205
315
|
throw error;
|
|
@@ -215,60 +325,621 @@ async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir,
|
|
|
215
325
|
}
|
|
216
326
|
}
|
|
217
327
|
}
|
|
328
|
+
async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
|
|
329
|
+
if (!existsSync(sourceProfileDir)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
await cp(sourceProfileDir, join(targetUserDataDir, profileDirectory), {
|
|
335
|
+
recursive: true
|
|
336
|
+
});
|
|
337
|
+
await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
|
|
338
|
+
await writePersistentProfileMetadata(targetUserDataDir, metadata);
|
|
339
|
+
}
|
|
340
|
+
async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
|
|
341
|
+
const tempUserDataDir = await mkdtemp(
|
|
342
|
+
buildPersistentProfileTempDirPrefix(persistentUserDataDir)
|
|
343
|
+
);
|
|
344
|
+
let published = false;
|
|
345
|
+
try {
|
|
346
|
+
const baseEntries = deserializeSnapshotManifestEntries(
|
|
347
|
+
runtimeMetadata.baseEntries
|
|
348
|
+
);
|
|
349
|
+
const currentEntries = await collectPersistentSnapshotEntries(
|
|
350
|
+
persistentUserDataDir,
|
|
351
|
+
metadata.profileDirectory
|
|
352
|
+
);
|
|
353
|
+
const runtimeEntries = await collectPersistentSnapshotEntries(
|
|
354
|
+
runtimeUserDataDir,
|
|
355
|
+
metadata.profileDirectory
|
|
356
|
+
);
|
|
357
|
+
const mergedEntries = resolveMergedSnapshotEntries(
|
|
358
|
+
baseEntries,
|
|
359
|
+
currentEntries,
|
|
360
|
+
runtimeEntries
|
|
361
|
+
);
|
|
362
|
+
await materializeMergedPersistentProfileSnapshot(
|
|
363
|
+
tempUserDataDir,
|
|
364
|
+
currentEntries,
|
|
365
|
+
runtimeEntries,
|
|
366
|
+
mergedEntries
|
|
367
|
+
);
|
|
368
|
+
await writePersistentProfileMetadata(tempUserDataDir, metadata);
|
|
369
|
+
await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
|
|
370
|
+
published = true;
|
|
371
|
+
} finally {
|
|
372
|
+
if (!published) {
|
|
373
|
+
await rm(tempUserDataDir, {
|
|
374
|
+
recursive: true,
|
|
375
|
+
force: true
|
|
376
|
+
}).catch(() => void 0);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function buildRuntimeProfileMetadata(runtimeUserDataDir, profileDirectory) {
|
|
381
|
+
if (!profileDirectory) {
|
|
382
|
+
return {
|
|
383
|
+
baseEntries: {},
|
|
384
|
+
profileDirectory: null
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const baseEntries = await collectPersistentSnapshotEntries(
|
|
388
|
+
runtimeUserDataDir,
|
|
389
|
+
profileDirectory
|
|
390
|
+
);
|
|
391
|
+
return {
|
|
392
|
+
baseEntries: serializeSnapshotManifestEntries(baseEntries),
|
|
393
|
+
profileDirectory
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async function writeRuntimeProfileMetadata(userDataDir, metadata) {
|
|
397
|
+
await writeFile(
|
|
398
|
+
join(userDataDir, OPENSTEER_RUNTIME_META_FILE),
|
|
399
|
+
JSON.stringify(metadata, null, 2)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
async function readRuntimeProfileMetadata(userDataDir) {
|
|
403
|
+
try {
|
|
404
|
+
const raw = await readFile(
|
|
405
|
+
join(userDataDir, OPENSTEER_RUNTIME_META_FILE),
|
|
406
|
+
"utf8"
|
|
407
|
+
);
|
|
408
|
+
const parsed = JSON.parse(raw);
|
|
409
|
+
const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
|
|
410
|
+
if (profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const baseEntries = deserializeSnapshotManifestEntries(
|
|
414
|
+
parsed.baseEntries
|
|
415
|
+
);
|
|
416
|
+
return {
|
|
417
|
+
baseEntries: Object.fromEntries(baseEntries),
|
|
418
|
+
profileDirectory
|
|
419
|
+
};
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function requireRuntimeProfileMetadata(userDataDir, expectedProfileDirectory) {
|
|
425
|
+
const metadata = await readRuntimeProfileMetadata(userDataDir);
|
|
426
|
+
if (!metadata) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Runtime profile metadata was not found for "${userDataDir}".`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (metadata.profileDirectory !== expectedProfileDirectory) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Runtime profile "${userDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return metadata;
|
|
437
|
+
}
|
|
438
|
+
async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
|
|
439
|
+
let rootEntries;
|
|
440
|
+
try {
|
|
441
|
+
rootEntries = await readdir(userDataDir, {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
withFileTypes: true
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
return /* @__PURE__ */ new Map();
|
|
447
|
+
}
|
|
448
|
+
rootEntries.sort((left, right) => left.name.localeCompare(right.name));
|
|
449
|
+
const collected = /* @__PURE__ */ new Map();
|
|
450
|
+
for (const entry of rootEntries) {
|
|
451
|
+
if (!shouldIncludePersistentRootEntry(
|
|
452
|
+
userDataDir,
|
|
453
|
+
profileDirectory,
|
|
454
|
+
entry.name
|
|
455
|
+
)) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
await collectSnapshotEntry(
|
|
459
|
+
join(userDataDir, entry.name),
|
|
460
|
+
entry.name,
|
|
461
|
+
collected
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return collected;
|
|
465
|
+
}
|
|
466
|
+
function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
|
|
467
|
+
if (entry === profileDirectory) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
if (COPY_SKIP_ENTRIES.has(entry)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return !isProfileDirectory(userDataDir, entry);
|
|
477
|
+
}
|
|
478
|
+
async function collectSnapshotEntry(sourcePath, relativePath, collected) {
|
|
479
|
+
let entryStat;
|
|
480
|
+
try {
|
|
481
|
+
entryStat = await stat(sourcePath);
|
|
482
|
+
} catch {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (entryStat.isDirectory()) {
|
|
486
|
+
collected.set(relativePath, {
|
|
487
|
+
kind: "directory",
|
|
488
|
+
hash: null,
|
|
489
|
+
sourcePath
|
|
490
|
+
});
|
|
491
|
+
let children;
|
|
492
|
+
try {
|
|
493
|
+
children = await readdir(sourcePath, {
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
withFileTypes: true
|
|
496
|
+
});
|
|
497
|
+
} catch {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
children.sort((left, right) => left.name.localeCompare(right.name));
|
|
501
|
+
for (const child of children) {
|
|
502
|
+
await collectSnapshotEntry(
|
|
503
|
+
join(sourcePath, child.name),
|
|
504
|
+
join(relativePath, child.name),
|
|
505
|
+
collected
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (entryStat.isFile()) {
|
|
511
|
+
collected.set(relativePath, {
|
|
512
|
+
kind: "file",
|
|
513
|
+
hash: await hashFile(sourcePath),
|
|
514
|
+
sourcePath
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function serializeSnapshotManifestEntries(entries) {
|
|
519
|
+
return Object.fromEntries(
|
|
520
|
+
[...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
|
|
521
|
+
relativePath,
|
|
522
|
+
{
|
|
523
|
+
kind: entry.kind,
|
|
524
|
+
hash: entry.hash
|
|
525
|
+
}
|
|
526
|
+
])
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
function deserializeSnapshotManifestEntries(entries) {
|
|
530
|
+
const manifestEntries = /* @__PURE__ */ new Map();
|
|
531
|
+
for (const [relativePath, entry] of Object.entries(entries)) {
|
|
532
|
+
if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Runtime profile metadata for "${relativePath}" is invalid.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
manifestEntries.set(relativePath, {
|
|
538
|
+
kind: entry.kind,
|
|
539
|
+
hash: entry.hash
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return manifestEntries;
|
|
543
|
+
}
|
|
544
|
+
function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
|
|
545
|
+
const mergedEntries = /* @__PURE__ */ new Map();
|
|
546
|
+
const relativePaths = /* @__PURE__ */ new Set([
|
|
547
|
+
...baseEntries.keys(),
|
|
548
|
+
...currentEntries.keys(),
|
|
549
|
+
...runtimeEntries.keys()
|
|
550
|
+
]);
|
|
551
|
+
for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
|
|
552
|
+
mergedEntries.set(
|
|
553
|
+
relativePath,
|
|
554
|
+
resolveMergedSnapshotEntrySelection(
|
|
555
|
+
relativePath,
|
|
556
|
+
baseEntries.get(relativePath) ?? null,
|
|
557
|
+
currentEntries.get(relativePath) ?? null,
|
|
558
|
+
runtimeEntries.get(relativePath) ?? null
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return mergedEntries;
|
|
563
|
+
}
|
|
564
|
+
function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
|
|
565
|
+
if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
|
|
566
|
+
return currentEntry ? "current" : null;
|
|
567
|
+
}
|
|
568
|
+
if (snapshotEntriesEqual(currentEntry, baseEntry)) {
|
|
569
|
+
return runtimeEntry ? "runtime" : null;
|
|
570
|
+
}
|
|
571
|
+
if (!baseEntry) {
|
|
572
|
+
if (!currentEntry) {
|
|
573
|
+
return runtimeEntry ? "runtime" : null;
|
|
574
|
+
}
|
|
575
|
+
if (!runtimeEntry) {
|
|
576
|
+
return "current";
|
|
577
|
+
}
|
|
578
|
+
if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
|
|
579
|
+
return "current";
|
|
580
|
+
}
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (!currentEntry && !runtimeEntry) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
|
|
589
|
+
return currentEntry ? "current" : null;
|
|
590
|
+
}
|
|
591
|
+
throw new Error(
|
|
592
|
+
`Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
function snapshotEntriesEqual(left, right) {
|
|
596
|
+
if (!left || !right) {
|
|
597
|
+
return left === right;
|
|
598
|
+
}
|
|
599
|
+
return left.kind === right.kind && left.hash === right.hash;
|
|
600
|
+
}
|
|
601
|
+
async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
|
|
602
|
+
const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
|
|
603
|
+
([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
|
|
604
|
+
);
|
|
605
|
+
for (const [relativePath, selection] of selectedEntries) {
|
|
606
|
+
const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
|
|
607
|
+
if (!entry) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const targetPath = join(targetUserDataDir, relativePath);
|
|
611
|
+
if (entry.kind === "directory") {
|
|
612
|
+
await mkdir(targetPath, { recursive: true });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
616
|
+
await copyFile(entry.sourcePath, targetPath);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function compareSnapshotPaths(left, right) {
|
|
620
|
+
const leftDepth = left.split(sep).length;
|
|
621
|
+
const rightDepth = right.split(sep).length;
|
|
622
|
+
if (leftDepth !== rightDepth) {
|
|
623
|
+
return leftDepth - rightDepth;
|
|
624
|
+
}
|
|
625
|
+
return left.localeCompare(right);
|
|
626
|
+
}
|
|
627
|
+
async function hashFile(filePath) {
|
|
628
|
+
return new Promise((resolve, reject) => {
|
|
629
|
+
const hash = createHash("sha256");
|
|
630
|
+
const stream = createReadStream(filePath);
|
|
631
|
+
stream.on("data", (chunk) => {
|
|
632
|
+
hash.update(chunk);
|
|
633
|
+
});
|
|
634
|
+
stream.on("error", reject);
|
|
635
|
+
stream.on("end", () => {
|
|
636
|
+
resolve(hash.digest("hex"));
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
}
|
|
218
640
|
async function ensurePersistentProfileMetadata(userDataDir, metadata) {
|
|
219
641
|
if (existsSync(join(userDataDir, OPENSTEER_META_FILE))) {
|
|
220
642
|
return;
|
|
221
643
|
}
|
|
222
644
|
await writePersistentProfileMetadata(userDataDir, metadata);
|
|
223
645
|
}
|
|
224
|
-
function
|
|
646
|
+
async function readPersistentProfileMetadata(userDataDir) {
|
|
647
|
+
try {
|
|
648
|
+
const raw = await readFile(join(userDataDir, OPENSTEER_META_FILE), "utf8");
|
|
649
|
+
const parsed = JSON.parse(raw);
|
|
650
|
+
if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
createdAt: parsed.createdAt,
|
|
655
|
+
profileDirectory: parsed.profileDirectory,
|
|
656
|
+
source: parsed.source
|
|
657
|
+
};
|
|
658
|
+
} catch {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function requirePersistentProfileMetadata(userDataDir) {
|
|
663
|
+
const metadata = await readPersistentProfileMetadata(userDataDir);
|
|
664
|
+
if (!metadata) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`Persistent profile metadata was not found for "${userDataDir}".`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return metadata;
|
|
670
|
+
}
|
|
671
|
+
function wasDirPublishedByAnotherProcess(error, targetDirPath) {
|
|
225
672
|
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
226
|
-
return existsSync(
|
|
673
|
+
return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
|
|
674
|
+
}
|
|
675
|
+
async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
|
|
676
|
+
if (!existsSync(targetUserDataDir)) {
|
|
677
|
+
await rename(replacementUserDataDir, targetUserDataDir);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
|
|
681
|
+
let targetMovedToBackup = false;
|
|
682
|
+
let replacementPublished = false;
|
|
683
|
+
try {
|
|
684
|
+
await rename(targetUserDataDir, backupUserDataDir);
|
|
685
|
+
targetMovedToBackup = true;
|
|
686
|
+
await rename(replacementUserDataDir, targetUserDataDir);
|
|
687
|
+
replacementPublished = true;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (targetMovedToBackup && !existsSync(targetUserDataDir)) {
|
|
690
|
+
await rename(backupUserDataDir, targetUserDataDir).catch(() => void 0);
|
|
691
|
+
}
|
|
692
|
+
throw error;
|
|
693
|
+
} finally {
|
|
694
|
+
if (replacementPublished && targetMovedToBackup && existsSync(backupUserDataDir)) {
|
|
695
|
+
await rm(backupUserDataDir, {
|
|
696
|
+
recursive: true,
|
|
697
|
+
force: true
|
|
698
|
+
}).catch(() => void 0);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function withPersistentProfileLock(targetUserDataDir, action) {
|
|
703
|
+
const lockDirPath = buildPersistentProfileLockDirPath(targetUserDataDir);
|
|
704
|
+
await mkdir(dirname(lockDirPath), { recursive: true });
|
|
705
|
+
while (true) {
|
|
706
|
+
const tempLockDirPath = `${lockDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${randomUUID()}`;
|
|
707
|
+
try {
|
|
708
|
+
await mkdir(tempLockDirPath);
|
|
709
|
+
await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_LOCK_OWNER);
|
|
710
|
+
try {
|
|
711
|
+
await rename(tempLockDirPath, lockDirPath);
|
|
712
|
+
break;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} finally {
|
|
719
|
+
await rm(tempLockDirPath, {
|
|
720
|
+
recursive: true,
|
|
721
|
+
force: true
|
|
722
|
+
}).catch(() => void 0);
|
|
723
|
+
}
|
|
724
|
+
const owner = await readLockOwner(lockDirPath);
|
|
725
|
+
if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
await sleep(PROFILE_LOCK_RETRY_DELAY_MS);
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
return await action();
|
|
732
|
+
} finally {
|
|
733
|
+
await rm(lockDirPath, {
|
|
734
|
+
recursive: true,
|
|
735
|
+
force: true
|
|
736
|
+
}).catch(() => void 0);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function writeLockOwner(lockDirPath, owner) {
|
|
740
|
+
await writeLockParticipant(join(lockDirPath, LOCK_OWNER_FILE), owner);
|
|
741
|
+
}
|
|
742
|
+
async function readLockOwner(lockDirPath) {
|
|
743
|
+
return await readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
|
|
744
|
+
}
|
|
745
|
+
async function writeLockParticipant(filePath, owner, options) {
|
|
746
|
+
await writeFile(filePath, JSON.stringify(owner), options);
|
|
747
|
+
}
|
|
748
|
+
async function readLockParticipant(filePath) {
|
|
749
|
+
return (await readLockParticipantRecord(filePath)).owner;
|
|
750
|
+
}
|
|
751
|
+
async function readLockReclaimerRecord(lockDirPath) {
|
|
752
|
+
return await readLockParticipantRecord(
|
|
753
|
+
join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
async function readLockParticipantRecord(filePath) {
|
|
757
|
+
try {
|
|
758
|
+
const raw = await readFile(filePath, "utf8");
|
|
759
|
+
const parsed = JSON.parse(raw);
|
|
760
|
+
const pid = Number(parsed.pid);
|
|
761
|
+
const processStartedAtMs = Number(parsed.processStartedAtMs);
|
|
762
|
+
if (!Number.isInteger(pid) || !Number.isInteger(processStartedAtMs)) {
|
|
763
|
+
return {
|
|
764
|
+
exists: true,
|
|
765
|
+
owner: null
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
exists: true,
|
|
770
|
+
owner: {
|
|
771
|
+
pid,
|
|
772
|
+
processStartedAtMs
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
} catch (error) {
|
|
776
|
+
return {
|
|
777
|
+
exists: getErrorCode(error) !== "ENOENT",
|
|
778
|
+
owner: null
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
|
|
783
|
+
if (!await tryAcquireLockReclaimer(lockDirPath)) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
let reclaimed = false;
|
|
787
|
+
try {
|
|
788
|
+
const owner = await readLockOwner(lockDirPath);
|
|
789
|
+
if (!lockOwnersEqual(owner, expectedOwner)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
if (owner && await getProcessLiveness(owner) !== "dead") {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
await rm(lockDirPath, {
|
|
796
|
+
recursive: true,
|
|
797
|
+
force: true
|
|
798
|
+
}).catch(() => void 0);
|
|
799
|
+
reclaimed = !existsSync(lockDirPath);
|
|
800
|
+
return reclaimed;
|
|
801
|
+
} finally {
|
|
802
|
+
if (!reclaimed) {
|
|
803
|
+
await rm(buildLockReclaimerDirPath(lockDirPath), {
|
|
804
|
+
recursive: true,
|
|
805
|
+
force: true
|
|
806
|
+
}).catch(() => void 0);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function tryAcquireLockReclaimer(lockDirPath) {
|
|
811
|
+
const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
|
|
812
|
+
while (true) {
|
|
813
|
+
const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${PROCESS_STARTED_AT_MS}-${randomUUID()}`;
|
|
814
|
+
try {
|
|
815
|
+
await mkdir(tempReclaimerDirPath);
|
|
816
|
+
await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_LOCK_OWNER);
|
|
817
|
+
try {
|
|
818
|
+
await rename(tempReclaimerDirPath, reclaimerDirPath);
|
|
819
|
+
return true;
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
const code = getErrorCode(error);
|
|
830
|
+
if (code === "ENOENT") {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
throw error;
|
|
834
|
+
} finally {
|
|
835
|
+
await rm(tempReclaimerDirPath, {
|
|
836
|
+
recursive: true,
|
|
837
|
+
force: true
|
|
838
|
+
}).catch(() => void 0);
|
|
839
|
+
}
|
|
840
|
+
const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
|
|
841
|
+
if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
await rm(reclaimerDirPath, {
|
|
848
|
+
recursive: true,
|
|
849
|
+
force: true
|
|
850
|
+
}).catch(() => void 0);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function lockOwnersEqual(left, right) {
|
|
854
|
+
if (!left || !right) {
|
|
855
|
+
return left === right;
|
|
856
|
+
}
|
|
857
|
+
return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
|
|
858
|
+
}
|
|
859
|
+
async function getProcessLiveness(owner) {
|
|
860
|
+
if (owner.pid === process.pid && hasMatchingProcessStartTime(
|
|
861
|
+
owner.processStartedAtMs,
|
|
862
|
+
PROCESS_STARTED_AT_MS
|
|
863
|
+
)) {
|
|
864
|
+
return "live";
|
|
865
|
+
}
|
|
866
|
+
const startedAtMs = await readProcessStartedAtMs(owner.pid);
|
|
867
|
+
if (typeof startedAtMs === "number") {
|
|
868
|
+
return hasMatchingProcessStartTime(
|
|
869
|
+
owner.processStartedAtMs,
|
|
870
|
+
startedAtMs
|
|
871
|
+
) ? "live" : "dead";
|
|
872
|
+
}
|
|
873
|
+
return isProcessRunning(owner.pid) ? "unknown" : "dead";
|
|
874
|
+
}
|
|
875
|
+
function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
|
|
876
|
+
return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
|
|
227
877
|
}
|
|
228
878
|
function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
|
|
229
879
|
return join(
|
|
230
880
|
dirname(targetUserDataDir),
|
|
231
|
-
`${
|
|
881
|
+
`${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
|
|
885
|
+
return `${basename(targetUserDataDir)}-tmp-`;
|
|
886
|
+
}
|
|
887
|
+
function buildPersistentProfileBackupDirPath(targetUserDataDir) {
|
|
888
|
+
return join(
|
|
889
|
+
dirname(targetUserDataDir),
|
|
890
|
+
`${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-backup-${Date.now()}`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
function buildPersistentProfileLockDirPath(targetUserDataDir) {
|
|
894
|
+
return join(dirname(targetUserDataDir), `${basename(targetUserDataDir)}.lock`);
|
|
895
|
+
}
|
|
896
|
+
function buildLockReclaimerDirPath(lockDirPath) {
|
|
897
|
+
return join(lockDirPath, LOCK_RECLAIMER_DIR);
|
|
898
|
+
}
|
|
899
|
+
function buildRuntimeProfileKey(sourceUserDataDir) {
|
|
900
|
+
const hash = createHash("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
|
|
901
|
+
return `${sanitizePathSegment(basename(sourceUserDataDir) || "profile")}-${hash}`;
|
|
902
|
+
}
|
|
903
|
+
function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
|
|
904
|
+
return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
|
|
905
|
+
}
|
|
906
|
+
function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
|
|
907
|
+
return join(
|
|
908
|
+
runtimesRootDir,
|
|
909
|
+
`${buildRuntimeProfileDirNamePrefix(sourceUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
|
|
232
910
|
);
|
|
233
911
|
}
|
|
234
|
-
async function
|
|
912
|
+
async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
|
|
235
913
|
let entries;
|
|
236
914
|
try {
|
|
237
|
-
entries = await readdir(
|
|
915
|
+
entries = await readdir(rootDir, {
|
|
238
916
|
encoding: "utf8",
|
|
239
917
|
withFileTypes: true
|
|
240
918
|
});
|
|
241
919
|
} catch {
|
|
242
920
|
return;
|
|
243
921
|
}
|
|
244
|
-
const tempDirPrefix = `${targetBaseName}-tmp-`;
|
|
245
922
|
await Promise.all(
|
|
246
923
|
entries.map(async (entry) => {
|
|
247
|
-
if (!entry.isDirectory() || !entry.name.startsWith(
|
|
924
|
+
if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
|
|
248
925
|
return;
|
|
249
926
|
}
|
|
250
|
-
if (
|
|
927
|
+
if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
|
|
251
928
|
return;
|
|
252
929
|
}
|
|
253
|
-
await rm(join(
|
|
930
|
+
await rm(join(rootDir, entry.name), {
|
|
254
931
|
recursive: true,
|
|
255
932
|
force: true
|
|
256
933
|
}).catch(() => void 0);
|
|
257
934
|
})
|
|
258
935
|
);
|
|
259
936
|
}
|
|
260
|
-
function
|
|
261
|
-
const owner =
|
|
262
|
-
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
if (owner.pid === process.pid && Math.abs(owner.processStartedAtMs - PROCESS_STARTED_AT_MS) <= PROCESS_START_TIME_TOLERANCE_MS) {
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
return isProcessRunning(owner.pid);
|
|
937
|
+
async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
|
|
938
|
+
const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
|
|
939
|
+
return owner ? await getProcessLiveness(owner) !== "dead" : false;
|
|
269
940
|
}
|
|
270
|
-
function
|
|
271
|
-
const remainder =
|
|
941
|
+
function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
|
|
942
|
+
const remainder = ownedDirName.slice(ownedDirPrefix.length);
|
|
272
943
|
const firstDashIndex = remainder.indexOf("-");
|
|
273
944
|
const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
|
|
274
945
|
if (firstDashIndex === -1 || secondDashIndex === -1) {
|
|
@@ -296,6 +967,120 @@ function isProcessRunning(pid) {
|
|
|
296
967
|
return code !== "ESRCH";
|
|
297
968
|
}
|
|
298
969
|
}
|
|
970
|
+
async function readProcessStartedAtMs(pid) {
|
|
971
|
+
if (pid <= 0) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
if (process.platform === "linux") {
|
|
975
|
+
return await readLinuxProcessStartedAtMs(pid);
|
|
976
|
+
}
|
|
977
|
+
if (process.platform === "win32") {
|
|
978
|
+
return await readWindowsProcessStartedAtMs(pid);
|
|
979
|
+
}
|
|
980
|
+
return await readPsProcessStartedAtMs(pid);
|
|
981
|
+
}
|
|
982
|
+
async function readLinuxProcessStartedAtMs(pid) {
|
|
983
|
+
let statRaw;
|
|
984
|
+
try {
|
|
985
|
+
statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
986
|
+
} catch (error) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
const startTicks = parseLinuxProcessStartTicks(statRaw);
|
|
990
|
+
if (startTicks === null) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
|
|
994
|
+
readLinuxBootTimeMs(),
|
|
995
|
+
readLinuxClockTicksPerSecond()
|
|
996
|
+
]);
|
|
997
|
+
if (bootTimeMs === null || clockTicksPerSecond === null) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
return Math.floor(
|
|
1001
|
+
bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
function parseLinuxProcessStartTicks(statRaw) {
|
|
1005
|
+
const closingParenIndex = statRaw.lastIndexOf(")");
|
|
1006
|
+
if (closingParenIndex === -1) {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
|
|
1010
|
+
const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
|
|
1011
|
+
return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
|
|
1012
|
+
}
|
|
1013
|
+
async function readLinuxBootTimeMs() {
|
|
1014
|
+
try {
|
|
1015
|
+
const statRaw = await readFile("/proc/stat", "utf8");
|
|
1016
|
+
const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
|
|
1017
|
+
if (!bootTimeLine) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const bootTimeSeconds = Number.parseInt(
|
|
1021
|
+
bootTimeLine.slice("btime ".length),
|
|
1022
|
+
10
|
|
1023
|
+
);
|
|
1024
|
+
return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
|
|
1025
|
+
} catch {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async function readLinuxClockTicksPerSecond() {
|
|
1030
|
+
linuxClockTicksPerSecondPromise ??= execFileAsync(
|
|
1031
|
+
"getconf",
|
|
1032
|
+
["CLK_TCK"]
|
|
1033
|
+
).then(
|
|
1034
|
+
({ stdout }) => {
|
|
1035
|
+
const value = Number.parseInt(stdout.trim(), 10);
|
|
1036
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
1037
|
+
},
|
|
1038
|
+
() => null
|
|
1039
|
+
);
|
|
1040
|
+
return await linuxClockTicksPerSecondPromise;
|
|
1041
|
+
}
|
|
1042
|
+
async function readPsProcessStartedAtMs(pid) {
|
|
1043
|
+
try {
|
|
1044
|
+
const { stdout } = await execFileAsync(
|
|
1045
|
+
"ps",
|
|
1046
|
+
["-p", String(pid), "-o", "lstart="],
|
|
1047
|
+
{ env: PS_COMMAND_ENV }
|
|
1048
|
+
);
|
|
1049
|
+
return parsePsStartedAtMs(stdout);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function parsePsStartedAtMs(stdout) {
|
|
1055
|
+
const raw = stdout.trim();
|
|
1056
|
+
if (!raw) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
const startedAtMs = Date.parse(raw);
|
|
1060
|
+
return Number.isNaN(startedAtMs) ? null : startedAtMs;
|
|
1061
|
+
}
|
|
1062
|
+
async function readWindowsProcessStartedAtMs(pid) {
|
|
1063
|
+
const script = [
|
|
1064
|
+
"$process = Get-Process -Id " + String(pid) + " -ErrorAction SilentlyContinue",
|
|
1065
|
+
"if ($null -eq $process) { exit 3 }",
|
|
1066
|
+
'$process.StartTime.ToUniversalTime().ToString("o")'
|
|
1067
|
+
].join("; ");
|
|
1068
|
+
try {
|
|
1069
|
+
const { stdout } = await execFileAsync(
|
|
1070
|
+
"powershell.exe",
|
|
1071
|
+
["-NoLogo", "-NoProfile", "-Command", script]
|
|
1072
|
+
);
|
|
1073
|
+
return parsePsStartedAtMs(stdout);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function getErrorCode(error) {
|
|
1079
|
+
return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
|
|
1080
|
+
}
|
|
1081
|
+
async function sleep(ms) {
|
|
1082
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1083
|
+
}
|
|
299
1084
|
|
|
300
1085
|
// src/browser/pool.ts
|
|
301
1086
|
import { spawn } from "child_process";
|
|
@@ -724,14 +1509,13 @@ var BrowserPool = class {
|
|
|
724
1509
|
browser = null;
|
|
725
1510
|
cdpProxy = null;
|
|
726
1511
|
launchedProcess = null;
|
|
727
|
-
|
|
728
|
-
persistentProfile = false;
|
|
1512
|
+
managedRuntimeProfile = null;
|
|
729
1513
|
defaults;
|
|
730
1514
|
constructor(defaults = {}) {
|
|
731
1515
|
this.defaults = defaults;
|
|
732
1516
|
}
|
|
733
1517
|
async launch(options = {}) {
|
|
734
|
-
if (this.browser || this.cdpProxy || this.launchedProcess || this.
|
|
1518
|
+
if (this.browser || this.cdpProxy || this.launchedProcess || this.managedRuntimeProfile) {
|
|
735
1519
|
await this.close();
|
|
736
1520
|
}
|
|
737
1521
|
const mode = options.mode ?? this.defaults.mode ?? "chromium";
|
|
@@ -781,13 +1565,10 @@ var BrowserPool = class {
|
|
|
781
1565
|
const browser = this.browser;
|
|
782
1566
|
const cdpProxy = this.cdpProxy;
|
|
783
1567
|
const launchedProcess = this.launchedProcess;
|
|
784
|
-
const
|
|
785
|
-
const persistentProfile = this.persistentProfile;
|
|
1568
|
+
const managedRuntimeProfile = this.managedRuntimeProfile;
|
|
786
1569
|
this.browser = null;
|
|
787
1570
|
this.cdpProxy = null;
|
|
788
1571
|
this.launchedProcess = null;
|
|
789
|
-
this.managedUserDataDir = null;
|
|
790
|
-
this.persistentProfile = false;
|
|
791
1572
|
try {
|
|
792
1573
|
if (browser) {
|
|
793
1574
|
await browser.close().catch(() => void 0);
|
|
@@ -795,11 +1576,14 @@ var BrowserPool = class {
|
|
|
795
1576
|
} finally {
|
|
796
1577
|
cdpProxy?.close();
|
|
797
1578
|
await killProcessTree(launchedProcess);
|
|
798
|
-
if (
|
|
799
|
-
await
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1579
|
+
if (managedRuntimeProfile) {
|
|
1580
|
+
await persistIsolatedRuntimeProfile(
|
|
1581
|
+
managedRuntimeProfile.userDataDir,
|
|
1582
|
+
managedRuntimeProfile.persistentUserDataDir
|
|
1583
|
+
);
|
|
1584
|
+
if (this.managedRuntimeProfile === managedRuntimeProfile) {
|
|
1585
|
+
this.managedRuntimeProfile = null;
|
|
1586
|
+
}
|
|
803
1587
|
}
|
|
804
1588
|
}
|
|
805
1589
|
}
|
|
@@ -849,7 +1633,9 @@ var BrowserPool = class {
|
|
|
849
1633
|
sourceUserDataDir,
|
|
850
1634
|
profileDirectory
|
|
851
1635
|
);
|
|
852
|
-
await
|
|
1636
|
+
const runtimeProfile = await createIsolatedRuntimeProfile(
|
|
1637
|
+
persistentProfile.userDataDir
|
|
1638
|
+
);
|
|
853
1639
|
const debugPort = await reserveDebugPort();
|
|
854
1640
|
const headless = resolveLaunchHeadless(
|
|
855
1641
|
"real",
|
|
@@ -857,7 +1643,7 @@ var BrowserPool = class {
|
|
|
857
1643
|
this.defaults.headless
|
|
858
1644
|
);
|
|
859
1645
|
const launchArgs = buildRealBrowserLaunchArgs({
|
|
860
|
-
userDataDir:
|
|
1646
|
+
userDataDir: runtimeProfile.userDataDir,
|
|
861
1647
|
profileDirectory,
|
|
862
1648
|
debugPort,
|
|
863
1649
|
headless
|
|
@@ -887,12 +1673,15 @@ var BrowserPool = class {
|
|
|
887
1673
|
}
|
|
888
1674
|
this.browser = browser;
|
|
889
1675
|
this.launchedProcess = processHandle;
|
|
890
|
-
this.
|
|
891
|
-
this.persistentProfile = true;
|
|
1676
|
+
this.managedRuntimeProfile = runtimeProfile;
|
|
892
1677
|
return { browser, context, page, isExternal: false };
|
|
893
1678
|
} catch (error) {
|
|
894
1679
|
await browser?.close().catch(() => void 0);
|
|
895
1680
|
await killProcessTree(processHandle);
|
|
1681
|
+
await rm2(runtimeProfile.userDataDir, {
|
|
1682
|
+
recursive: true,
|
|
1683
|
+
force: true
|
|
1684
|
+
}).catch(() => void 0);
|
|
896
1685
|
throw error;
|
|
897
1686
|
}
|
|
898
1687
|
}
|
|
@@ -1021,7 +1810,7 @@ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
|
|
|
1021
1810
|
} catch (error) {
|
|
1022
1811
|
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
1023
1812
|
}
|
|
1024
|
-
await
|
|
1813
|
+
await sleep2(100);
|
|
1025
1814
|
}
|
|
1026
1815
|
throw new Error(
|
|
1027
1816
|
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
@@ -1088,7 +1877,7 @@ async function killProcessTree(processHandle) {
|
|
|
1088
1877
|
}
|
|
1089
1878
|
}
|
|
1090
1879
|
}
|
|
1091
|
-
async function
|
|
1880
|
+
async function sleep2(ms) {
|
|
1092
1881
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1093
1882
|
}
|
|
1094
1883
|
|
|
@@ -1470,7 +2259,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
1470
2259
|
TRANSIENT_CONTEXT_RETRY_DELAY_MS,
|
|
1471
2260
|
Math.max(0, deadline - Date.now())
|
|
1472
2261
|
);
|
|
1473
|
-
await
|
|
2262
|
+
await sleep3(retryDelay);
|
|
1474
2263
|
}
|
|
1475
2264
|
}
|
|
1476
2265
|
}
|
|
@@ -1503,7 +2292,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
1503
2292
|
() => ({ kind: "resolved" }),
|
|
1504
2293
|
(error) => ({ kind: "rejected", error })
|
|
1505
2294
|
);
|
|
1506
|
-
const timeoutPromise =
|
|
2295
|
+
const timeoutPromise = sleep3(
|
|
1507
2296
|
timeout + FRAME_EVALUATE_GRACE_MS
|
|
1508
2297
|
).then(() => ({ kind: "timeout" }));
|
|
1509
2298
|
const result = await Promise.race([
|
|
@@ -1645,7 +2434,7 @@ function isIgnorableFrameError(error) {
|
|
|
1645
2434
|
const message = error.message;
|
|
1646
2435
|
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");
|
|
1647
2436
|
}
|
|
1648
|
-
function
|
|
2437
|
+
function sleep3(ms) {
|
|
1649
2438
|
return new Promise((resolve) => {
|
|
1650
2439
|
setTimeout(resolve, ms);
|
|
1651
2440
|
});
|
|
@@ -5897,7 +6686,7 @@ async function closeTab(context, activePage, index) {
|
|
|
5897
6686
|
}
|
|
5898
6687
|
|
|
5899
6688
|
// src/actions/cookies.ts
|
|
5900
|
-
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
6689
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
5901
6690
|
async function getCookies(context, url) {
|
|
5902
6691
|
return context.cookies(url ? [url] : void 0);
|
|
5903
6692
|
}
|
|
@@ -5912,7 +6701,7 @@ async function exportCookies(context, filePath, url) {
|
|
|
5912
6701
|
await writeFile2(filePath, JSON.stringify(cookies, null, 2), "utf-8");
|
|
5913
6702
|
}
|
|
5914
6703
|
async function importCookies(context, filePath) {
|
|
5915
|
-
const raw = await
|
|
6704
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
5916
6705
|
const cookies = JSON.parse(raw);
|
|
5917
6706
|
await context.addCookies(cookies);
|
|
5918
6707
|
}
|
|
@@ -7740,7 +8529,7 @@ async function executeAgentAction(page, action) {
|
|
|
7740
8529
|
}
|
|
7741
8530
|
case "wait": {
|
|
7742
8531
|
const ms = numberOr(action.timeMs, action.time_ms, 1e3);
|
|
7743
|
-
await
|
|
8532
|
+
await sleep4(ms);
|
|
7744
8533
|
return;
|
|
7745
8534
|
}
|
|
7746
8535
|
case "goto": {
|
|
@@ -7905,7 +8694,7 @@ async function pressKeyCombo(page, combo) {
|
|
|
7905
8694
|
}
|
|
7906
8695
|
}
|
|
7907
8696
|
}
|
|
7908
|
-
function
|
|
8697
|
+
function sleep4(ms) {
|
|
7909
8698
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7910
8699
|
}
|
|
7911
8700
|
|
|
@@ -7936,7 +8725,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
7936
8725
|
if (isMutatingAgentAction(action)) {
|
|
7937
8726
|
this.onMutatingAction?.(action);
|
|
7938
8727
|
}
|
|
7939
|
-
await
|
|
8728
|
+
await sleep5(this.config.waitBetweenActionsMs);
|
|
7940
8729
|
});
|
|
7941
8730
|
try {
|
|
7942
8731
|
const result = await this.client.execute({
|
|
@@ -7998,7 +8787,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
7998
8787
|
await this.cursorController.preview({ x, y }, "agent");
|
|
7999
8788
|
}
|
|
8000
8789
|
};
|
|
8001
|
-
function
|
|
8790
|
+
function sleep5(ms) {
|
|
8002
8791
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
8003
8792
|
}
|
|
8004
8793
|
|
|
@@ -8434,7 +9223,7 @@ var CursorController = class {
|
|
|
8434
9223
|
for (const step of motion.points) {
|
|
8435
9224
|
await this.renderer.move(step, this.style);
|
|
8436
9225
|
if (motion.stepDelayMs > 0) {
|
|
8437
|
-
await
|
|
9226
|
+
await sleep6(motion.stepDelayMs);
|
|
8438
9227
|
}
|
|
8439
9228
|
}
|
|
8440
9229
|
if (shouldPulse(intent)) {
|
|
@@ -8592,12 +9381,12 @@ function clamp2(value, min, max) {
|
|
|
8592
9381
|
function shouldPulse(intent) {
|
|
8593
9382
|
return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
|
|
8594
9383
|
}
|
|
8595
|
-
function
|
|
9384
|
+
function sleep6(ms) {
|
|
8596
9385
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8597
9386
|
}
|
|
8598
9387
|
|
|
8599
9388
|
// src/opensteer.ts
|
|
8600
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
9389
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
8601
9390
|
|
|
8602
9391
|
// src/action-wait.ts
|
|
8603
9392
|
var ROBUST_PROFILE = {
|
|
@@ -8792,7 +9581,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8792
9581
|
this.idleSince = 0;
|
|
8793
9582
|
}
|
|
8794
9583
|
const remaining = Math.max(1, options.deadline - now);
|
|
8795
|
-
await
|
|
9584
|
+
await sleep7(Math.min(NETWORK_POLL_MS, remaining));
|
|
8796
9585
|
}
|
|
8797
9586
|
}
|
|
8798
9587
|
handleRequestStarted = (request) => {
|
|
@@ -8837,7 +9626,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8837
9626
|
return false;
|
|
8838
9627
|
}
|
|
8839
9628
|
};
|
|
8840
|
-
async function
|
|
9629
|
+
async function sleep7(ms) {
|
|
8841
9630
|
await new Promise((resolve) => {
|
|
8842
9631
|
setTimeout(resolve, ms);
|
|
8843
9632
|
});
|
|
@@ -10617,15 +11406,18 @@ var Opensteer = class _Opensteer {
|
|
|
10617
11406
|
}
|
|
10618
11407
|
return;
|
|
10619
11408
|
}
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
11409
|
+
try {
|
|
11410
|
+
if (this.ownsBrowser) {
|
|
11411
|
+
await this.pool.close();
|
|
11412
|
+
}
|
|
11413
|
+
} finally {
|
|
11414
|
+
this.browser = null;
|
|
11415
|
+
this.pageRef = null;
|
|
11416
|
+
this.contextRef = null;
|
|
11417
|
+
this.ownsBrowser = false;
|
|
11418
|
+
if (this.cursorController) {
|
|
11419
|
+
await this.cursorController.dispose().catch(() => void 0);
|
|
11420
|
+
}
|
|
10629
11421
|
}
|
|
10630
11422
|
}
|
|
10631
11423
|
async syncLocalSelectorCacheToCloud() {
|
|
@@ -12949,12 +13741,14 @@ function normalizeCloudBrowserProfilePreference(value, source) {
|
|
|
12949
13741
|
}
|
|
12950
13742
|
function buildLocalRunId(namespace) {
|
|
12951
13743
|
const normalized = namespace.trim() || "default";
|
|
12952
|
-
return `${normalized}-${Date.now().toString(36)}-${
|
|
13744
|
+
return `${normalized}-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
|
|
12953
13745
|
}
|
|
12954
13746
|
|
|
12955
13747
|
export {
|
|
12956
13748
|
getOrCreatePersistentProfile,
|
|
12957
13749
|
clearPersistentProfileSingletons,
|
|
13750
|
+
createIsolatedRuntimeProfile,
|
|
13751
|
+
persistIsolatedRuntimeProfile,
|
|
12958
13752
|
BrowserPool,
|
|
12959
13753
|
waitForVisualStability,
|
|
12960
13754
|
createEmptyRegistry,
|