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.
- package/dist/{chunk-RC4IPQDZ.js → chunk-HBTSQ2V4.js} +2535 -1104
- package/dist/cli/profile.cjs +1800 -1121
- package/dist/cli/profile.js +1 -1
- package/dist/cli/server.cjs +1718 -1039
- package/dist/cli/server.js +1 -1
- package/dist/index.cjs +2527 -1102
- package/dist/index.d.cts +26 -17
- package/dist/index.d.ts +26 -17
- package/dist/index.js +5 -1
- package/package.json +1 -1
|
@@ -25,1069 +25,2655 @@ import {
|
|
|
25
25
|
} from "./chunk-3H5RRIMZ.js";
|
|
26
26
|
|
|
27
27
|
// src/browser/persistent-profile.ts
|
|
28
|
-
import { createHash, randomUUID } from "crypto";
|
|
29
|
-
import { execFile } from "child_process";
|
|
30
|
-
import { createReadStream, existsSync } from "fs";
|
|
28
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
29
|
+
import { execFile as execFile2 } from "child_process";
|
|
30
|
+
import { createReadStream, existsSync as existsSync3 } from "fs";
|
|
31
31
|
import {
|
|
32
32
|
cp,
|
|
33
33
|
copyFile,
|
|
34
|
-
mkdir,
|
|
34
|
+
mkdir as mkdir3,
|
|
35
35
|
mkdtemp,
|
|
36
|
-
readdir,
|
|
37
|
-
readFile,
|
|
38
|
-
rename,
|
|
39
|
-
rm,
|
|
36
|
+
readdir as readdir2,
|
|
37
|
+
readFile as readFile4,
|
|
38
|
+
rename as rename3,
|
|
39
|
+
rm as rm3,
|
|
40
40
|
stat,
|
|
41
|
-
writeFile
|
|
41
|
+
writeFile as writeFile3
|
|
42
42
|
} from "fs/promises";
|
|
43
43
|
import { homedir, tmpdir } from "os";
|
|
44
|
-
import { basename, dirname, join, relative, sep } from "path";
|
|
44
|
+
import { basename as basename3, dirname as dirname4, join as join4, relative, sep } from "path";
|
|
45
|
+
import { promisify as promisify2 } from "util";
|
|
46
|
+
|
|
47
|
+
// src/browser/persistent-profile-coordination.ts
|
|
48
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
49
|
+
|
|
50
|
+
// src/browser/dir-lock.ts
|
|
51
|
+
import { randomUUID } from "crypto";
|
|
52
|
+
import { existsSync } from "fs";
|
|
53
|
+
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
54
|
+
import { dirname, join } from "path";
|
|
55
|
+
|
|
56
|
+
// src/browser/process-owner.ts
|
|
57
|
+
import { execFile } from "child_process";
|
|
58
|
+
import { readFile } from "fs/promises";
|
|
45
59
|
import { promisify } from "util";
|
|
46
60
|
var execFileAsync = promisify(execFile);
|
|
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";
|
|
51
61
|
var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
|
|
52
62
|
var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
|
|
53
|
-
var
|
|
63
|
+
var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
54
64
|
var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
|
|
55
65
|
var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
|
|
56
|
-
var
|
|
57
|
-
"SingletonCookie",
|
|
58
|
-
"SingletonLock",
|
|
59
|
-
"SingletonSocket",
|
|
60
|
-
"DevToolsActivePort",
|
|
61
|
-
"lockfile"
|
|
62
|
-
]);
|
|
63
|
-
var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
|
|
64
|
-
...CHROME_SINGLETON_ENTRIES,
|
|
65
|
-
OPENSTEER_META_FILE,
|
|
66
|
-
OPENSTEER_RUNTIME_META_FILE
|
|
67
|
-
]);
|
|
68
|
-
var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
69
|
-
"Crash Reports",
|
|
70
|
-
"Crashpad",
|
|
71
|
-
"BrowserMetrics",
|
|
72
|
-
"GrShaderCache",
|
|
73
|
-
"ShaderCache",
|
|
74
|
-
"GraphiteDawnCache",
|
|
75
|
-
"component_crx_cache",
|
|
76
|
-
"Crowd Deny",
|
|
77
|
-
"hyphen-data",
|
|
78
|
-
"OnDeviceHeadSuggestModel",
|
|
79
|
-
"OptimizationGuidePredictionModels",
|
|
80
|
-
"Segmentation Platform",
|
|
81
|
-
"SmartCardDeviceNames",
|
|
82
|
-
"WidevineCdm",
|
|
83
|
-
"pnacl"
|
|
84
|
-
]);
|
|
85
|
-
var CURRENT_PROCESS_LOCK_OWNER = {
|
|
66
|
+
var CURRENT_PROCESS_OWNER = {
|
|
86
67
|
pid: process.pid,
|
|
87
68
|
processStartedAtMs: PROCESS_STARTED_AT_MS
|
|
88
69
|
};
|
|
89
70
|
var linuxClockTicksPerSecondPromise = null;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
const
|
|
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
|
-
});
|
|
125
|
-
}
|
|
126
|
-
async function clearPersistentProfileSingletons(userDataDir) {
|
|
127
|
-
await Promise.all(
|
|
128
|
-
[...CHROME_SINGLETON_ENTRIES].map(
|
|
129
|
-
(entry) => rm(join(userDataDir, entry), {
|
|
130
|
-
force: true,
|
|
131
|
-
recursive: true
|
|
132
|
-
}).catch(() => void 0)
|
|
133
|
-
)
|
|
134
|
-
);
|
|
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
|
-
}
|
|
210
|
-
function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
|
|
211
|
-
const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
|
|
212
|
-
const sourceLabel = sanitizePathSegment(basename(sourceUserDataDir) || "user-data");
|
|
213
|
-
const profileLabel = sanitizePathSegment(profileDirectory || "Default");
|
|
214
|
-
return `${sourceLabel}-${profileLabel}-${hash}`;
|
|
71
|
+
function parseProcessOwner(value) {
|
|
72
|
+
if (!value || typeof value !== "object") {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const parsed = value;
|
|
76
|
+
const pid = Number(parsed.pid);
|
|
77
|
+
const processStartedAtMs = Number(parsed.processStartedAtMs);
|
|
78
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
pid,
|
|
86
|
+
processStartedAtMs
|
|
87
|
+
};
|
|
215
88
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
89
|
+
function processOwnersEqual(left, right) {
|
|
90
|
+
if (!left || !right) {
|
|
91
|
+
return left === right;
|
|
92
|
+
}
|
|
93
|
+
return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
|
|
218
94
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
95
|
+
async function getProcessLiveness(owner) {
|
|
96
|
+
if (owner.pid === process.pid && hasMatchingProcessStartTime(
|
|
97
|
+
owner.processStartedAtMs,
|
|
98
|
+
PROCESS_STARTED_AT_MS
|
|
99
|
+
)) {
|
|
100
|
+
return "live";
|
|
101
|
+
}
|
|
102
|
+
const startedAtMs = await readProcessStartedAtMs(owner.pid);
|
|
103
|
+
if (typeof startedAtMs === "number") {
|
|
104
|
+
return hasMatchingProcessStartTime(
|
|
105
|
+
owner.processStartedAtMs,
|
|
106
|
+
startedAtMs
|
|
107
|
+
) ? "live" : "dead";
|
|
108
|
+
}
|
|
109
|
+
return isProcessRunning(owner.pid) ? "unknown" : "dead";
|
|
221
110
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
111
|
+
function isProcessRunning(pid) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
117
|
+
return code !== "ESRCH";
|
|
118
|
+
}
|
|
225
119
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
120
|
+
async function readProcessOwner(pid) {
|
|
121
|
+
const processStartedAtMs = await readProcessStartedAtMs(pid);
|
|
122
|
+
if (processStartedAtMs === null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
pid,
|
|
127
|
+
processStartedAtMs
|
|
128
|
+
};
|
|
228
129
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
recursive: true,
|
|
232
|
-
filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
|
|
233
|
-
});
|
|
130
|
+
function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
|
|
131
|
+
return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
|
|
234
132
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return true;
|
|
133
|
+
async function readProcessStartedAtMs(pid) {
|
|
134
|
+
if (pid <= 0) {
|
|
135
|
+
return null;
|
|
239
136
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return true;
|
|
137
|
+
if (process.platform === "linux") {
|
|
138
|
+
return await readLinuxProcessStartedAtMs(pid);
|
|
243
139
|
}
|
|
244
|
-
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
return await readWindowsProcessStartedAtMs(pid);
|
|
142
|
+
}
|
|
143
|
+
return await readPsProcessStartedAtMs(pid);
|
|
245
144
|
}
|
|
246
|
-
async function
|
|
247
|
-
let
|
|
145
|
+
async function readLinuxProcessStartedAtMs(pid) {
|
|
146
|
+
let statRaw;
|
|
248
147
|
try {
|
|
249
|
-
|
|
148
|
+
statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
250
149
|
} catch {
|
|
251
|
-
return;
|
|
150
|
+
return null;
|
|
252
151
|
}
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (entry === targetProfileDirectory) continue;
|
|
257
|
-
const sourcePath = join(sourceUserDataDir, entry);
|
|
258
|
-
const targetPath = join(targetUserDataDir, entry);
|
|
259
|
-
if (existsSync(targetPath)) continue;
|
|
260
|
-
let entryStat;
|
|
261
|
-
try {
|
|
262
|
-
entryStat = await stat(sourcePath);
|
|
263
|
-
} catch {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (entryStat.isFile()) {
|
|
267
|
-
copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
|
|
268
|
-
} else if (entryStat.isDirectory()) {
|
|
269
|
-
if (isProfileDirectory(sourceUserDataDir, entry)) continue;
|
|
270
|
-
if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
|
|
271
|
-
copyTasks.push(
|
|
272
|
-
cp(sourcePath, targetPath, { recursive: true }).catch(
|
|
273
|
-
() => void 0
|
|
274
|
-
)
|
|
275
|
-
);
|
|
276
|
-
}
|
|
152
|
+
const startTicks = parseLinuxProcessStartTicks(statRaw);
|
|
153
|
+
if (startTicks === null) {
|
|
154
|
+
return null;
|
|
277
155
|
}
|
|
278
|
-
await Promise.all(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
156
|
+
const [bootTimeMs, clockTicksPerSecond] = await Promise.all([
|
|
157
|
+
readLinuxBootTimeMs(),
|
|
158
|
+
readLinuxClockTicksPerSecond()
|
|
159
|
+
]);
|
|
160
|
+
if (bootTimeMs === null || clockTicksPerSecond === null) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return Math.floor(
|
|
164
|
+
bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
|
|
284
165
|
);
|
|
285
166
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
source: sourceUserDataDir
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
|
|
294
|
-
if (existsSync(targetUserDataDir)) {
|
|
295
|
-
return false;
|
|
167
|
+
function parseLinuxProcessStartTicks(statRaw) {
|
|
168
|
+
const closingParenIndex = statRaw.lastIndexOf(")");
|
|
169
|
+
if (closingParenIndex === -1) {
|
|
170
|
+
return null;
|
|
296
171
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
);
|
|
300
|
-
|
|
172
|
+
const fields = statRaw.slice(closingParenIndex + 2).trim().split(/\s+/);
|
|
173
|
+
const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
|
|
174
|
+
return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
|
|
175
|
+
}
|
|
176
|
+
async function readLinuxBootTimeMs() {
|
|
301
177
|
try {
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
178
|
+
const statRaw = await readFile("/proc/stat", "utf8");
|
|
179
|
+
const bootTimeLine = statRaw.split("\n").find((line) => line.startsWith("btime "));
|
|
180
|
+
if (!bootTimeLine) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const bootTimeSeconds = Number.parseInt(
|
|
184
|
+
bootTimeLine.slice("btime ".length),
|
|
185
|
+
10
|
|
308
186
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
187
|
+
return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readLinuxClockTicksPerSecond() {
|
|
193
|
+
if (!linuxClockTicksPerSecondPromise) {
|
|
194
|
+
linuxClockTicksPerSecondPromise = execFileAsync("getconf", ["CLK_TCK"], {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
|
|
197
|
+
}).then(({ stdout }) => {
|
|
198
|
+
const value = Number.parseInt(stdout.trim(), 10);
|
|
199
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
200
|
+
}).catch(() => null);
|
|
201
|
+
}
|
|
202
|
+
return await linuxClockTicksPerSecondPromise;
|
|
203
|
+
}
|
|
204
|
+
async function readWindowsProcessStartedAtMs(pid) {
|
|
205
|
+
try {
|
|
206
|
+
const { stdout } = await execFileAsync(
|
|
207
|
+
"powershell.exe",
|
|
208
|
+
[
|
|
209
|
+
"-NoProfile",
|
|
210
|
+
"-Command",
|
|
211
|
+
`(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString("o")`
|
|
212
|
+
],
|
|
213
|
+
{
|
|
214
|
+
encoding: "utf8",
|
|
215
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
|
|
314
216
|
}
|
|
315
|
-
|
|
217
|
+
);
|
|
218
|
+
const isoTimestamp = stdout.trim();
|
|
219
|
+
if (!isoTimestamp) {
|
|
220
|
+
return null;
|
|
316
221
|
}
|
|
317
|
-
|
|
318
|
-
return
|
|
222
|
+
const startedAtMs = Date.parse(isoTimestamp);
|
|
223
|
+
return Number.isFinite(startedAtMs) ? startedAtMs : null;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function readPsProcessStartedAtMs(pid) {
|
|
229
|
+
try {
|
|
230
|
+
const { stdout } = await execFileAsync(
|
|
231
|
+
"ps",
|
|
232
|
+
["-o", "lstart=", "-p", String(pid)],
|
|
233
|
+
{
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
env: PS_COMMAND_ENV,
|
|
236
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
const startedAt = stdout.trim();
|
|
240
|
+
if (!startedAt) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const startedAtMs = Date.parse(startedAt.replace(/\s+/g, " "));
|
|
244
|
+
return Number.isFinite(startedAtMs) ? startedAtMs : null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/browser/dir-lock.ts
|
|
251
|
+
var LOCK_OWNER_FILE = "owner.json";
|
|
252
|
+
var LOCK_RECLAIMER_DIR = "reclaimer";
|
|
253
|
+
var LOCK_RETRY_DELAY_MS = 50;
|
|
254
|
+
async function withDirLock(lockDirPath, action) {
|
|
255
|
+
const releaseLock = await acquireDirLock(lockDirPath);
|
|
256
|
+
try {
|
|
257
|
+
return await action();
|
|
319
258
|
} finally {
|
|
320
|
-
|
|
321
|
-
|
|
259
|
+
await releaseLock();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function acquireDirLock(lockDirPath) {
|
|
263
|
+
while (true) {
|
|
264
|
+
const releaseLock = await tryAcquireDirLock(lockDirPath);
|
|
265
|
+
if (releaseLock) {
|
|
266
|
+
return releaseLock;
|
|
267
|
+
}
|
|
268
|
+
await sleep(LOCK_RETRY_DELAY_MS);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function tryAcquireDirLock(lockDirPath) {
|
|
272
|
+
await mkdir(dirname(lockDirPath), { recursive: true });
|
|
273
|
+
while (true) {
|
|
274
|
+
const tempLockDirPath = `${lockDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
|
|
275
|
+
try {
|
|
276
|
+
await mkdir(tempLockDirPath);
|
|
277
|
+
await writeLockOwner(tempLockDirPath, CURRENT_PROCESS_OWNER);
|
|
278
|
+
try {
|
|
279
|
+
await rename(tempLockDirPath, lockDirPath);
|
|
280
|
+
break;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (!wasDirPublishedByAnotherProcess(error, lockDirPath)) {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
await rm(tempLockDirPath, {
|
|
322
288
|
recursive: true,
|
|
323
289
|
force: true
|
|
324
290
|
}).catch(() => void 0);
|
|
325
291
|
}
|
|
292
|
+
const owner = await readLockOwner(lockDirPath);
|
|
293
|
+
if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
326
297
|
}
|
|
298
|
+
return async () => {
|
|
299
|
+
await rm(lockDirPath, {
|
|
300
|
+
recursive: true,
|
|
301
|
+
force: true
|
|
302
|
+
}).catch(() => void 0);
|
|
303
|
+
};
|
|
327
304
|
}
|
|
328
|
-
async function
|
|
329
|
-
if (!existsSync(
|
|
330
|
-
|
|
331
|
-
`Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
|
|
332
|
-
);
|
|
305
|
+
async function isDirLockHeld(lockDirPath) {
|
|
306
|
+
if (!existsSync(lockDirPath)) {
|
|
307
|
+
return false;
|
|
333
308
|
}
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
309
|
+
const owner = await readLockOwner(lockDirPath);
|
|
310
|
+
if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return existsSync(lockDirPath);
|
|
339
314
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
315
|
+
function getErrorCode(error) {
|
|
316
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
317
|
+
}
|
|
318
|
+
function wasDirPublishedByAnotherProcess(error, targetDirPath) {
|
|
319
|
+
const code = getErrorCode(error);
|
|
320
|
+
return existsSync(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
|
|
321
|
+
}
|
|
322
|
+
async function writeLockOwner(lockDirPath, owner) {
|
|
323
|
+
await writeFile(join(lockDirPath, LOCK_OWNER_FILE), JSON.stringify(owner));
|
|
324
|
+
}
|
|
325
|
+
async function readLockOwner(lockDirPath) {
|
|
326
|
+
return await readLockParticipant(join(lockDirPath, LOCK_OWNER_FILE));
|
|
327
|
+
}
|
|
328
|
+
async function readLockParticipant(filePath) {
|
|
329
|
+
return (await readLockParticipantRecord(filePath)).owner;
|
|
330
|
+
}
|
|
331
|
+
async function readLockParticipantRecord(filePath) {
|
|
332
|
+
try {
|
|
333
|
+
const raw = await readFile2(filePath, "utf8");
|
|
334
|
+
const owner = parseProcessOwner(JSON.parse(raw));
|
|
335
|
+
return {
|
|
336
|
+
exists: true,
|
|
337
|
+
owner
|
|
338
|
+
};
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return {
|
|
341
|
+
exists: getErrorCode(error) !== "ENOENT",
|
|
342
|
+
owner: null
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function readLockReclaimerRecord(lockDirPath) {
|
|
347
|
+
return await readLockParticipantRecord(
|
|
348
|
+
join(buildLockReclaimerDirPath(lockDirPath), LOCK_OWNER_FILE)
|
|
343
349
|
);
|
|
344
|
-
|
|
350
|
+
}
|
|
351
|
+
async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
|
|
352
|
+
if (!await tryAcquireLockReclaimer(lockDirPath)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
let reclaimed = false;
|
|
345
356
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
|
|
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;
|
|
357
|
+
const owner = await readLockOwner(lockDirPath);
|
|
358
|
+
if (!processOwnersEqual(owner, expectedOwner)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (owner && await getProcessLiveness(owner) !== "dead") {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
await rm(lockDirPath, {
|
|
365
|
+
recursive: true,
|
|
366
|
+
force: true
|
|
367
|
+
}).catch(() => void 0);
|
|
368
|
+
reclaimed = !existsSync(lockDirPath);
|
|
369
|
+
return reclaimed;
|
|
371
370
|
} finally {
|
|
372
|
-
if (!
|
|
373
|
-
await rm(
|
|
371
|
+
if (!reclaimed) {
|
|
372
|
+
await rm(buildLockReclaimerDirPath(lockDirPath), {
|
|
374
373
|
recursive: true,
|
|
375
374
|
force: true
|
|
376
375
|
}).catch(() => void 0);
|
|
377
376
|
}
|
|
378
377
|
}
|
|
379
378
|
}
|
|
380
|
-
async function
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
async function tryAcquireLockReclaimer(lockDirPath) {
|
|
380
|
+
const reclaimerDirPath = buildLockReclaimerDirPath(lockDirPath);
|
|
381
|
+
while (true) {
|
|
382
|
+
const tempReclaimerDirPath = `${reclaimerDirPath}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID()}`;
|
|
383
|
+
try {
|
|
384
|
+
await mkdir(tempReclaimerDirPath);
|
|
385
|
+
await writeLockOwner(tempReclaimerDirPath, CURRENT_PROCESS_OWNER);
|
|
386
|
+
try {
|
|
387
|
+
await rename(tempReclaimerDirPath, reclaimerDirPath);
|
|
388
|
+
return true;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (!wasDirPublishedByAnotherProcess(error, reclaimerDirPath)) {
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const code = getErrorCode(error);
|
|
399
|
+
if (code === "ENOENT") {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
} finally {
|
|
404
|
+
await rm(tempReclaimerDirPath, {
|
|
405
|
+
recursive: true,
|
|
406
|
+
force: true
|
|
407
|
+
}).catch(() => void 0);
|
|
408
|
+
}
|
|
409
|
+
const reclaimerRecord = await readLockReclaimerRecord(lockDirPath);
|
|
410
|
+
if (!reclaimerRecord.exists || !reclaimerRecord.owner) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
if (await getProcessLiveness(reclaimerRecord.owner) !== "dead") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
await rm(reclaimerDirPath, {
|
|
417
|
+
recursive: true,
|
|
418
|
+
force: true
|
|
419
|
+
}).catch(() => void 0);
|
|
386
420
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
421
|
+
}
|
|
422
|
+
function buildLockReclaimerDirPath(lockDirPath) {
|
|
423
|
+
return join(lockDirPath, LOCK_RECLAIMER_DIR);
|
|
424
|
+
}
|
|
425
|
+
async function sleep(ms) {
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/browser/persistent-profile-coordination.ts
|
|
430
|
+
var PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS = 50;
|
|
431
|
+
async function withPersistentProfileControlLock(targetUserDataDir, action) {
|
|
432
|
+
return await withDirLock(
|
|
433
|
+
buildPersistentProfileControlLockDirPath(targetUserDataDir),
|
|
434
|
+
action
|
|
390
435
|
);
|
|
391
|
-
return {
|
|
392
|
-
baseEntries: serializeSnapshotManifestEntries(baseEntries),
|
|
393
|
-
profileDirectory
|
|
394
|
-
};
|
|
395
436
|
}
|
|
396
|
-
async function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
437
|
+
async function acquirePersistentProfileWriteLock(targetUserDataDir) {
|
|
438
|
+
const controlLockDirPath = buildPersistentProfileControlLockDirPath(targetUserDataDir);
|
|
439
|
+
const writeLockDirPath = buildPersistentProfileWriteLockDirPath(
|
|
440
|
+
targetUserDataDir
|
|
441
|
+
);
|
|
442
|
+
while (true) {
|
|
443
|
+
let releaseWriteLock = null;
|
|
444
|
+
const releaseControlLock = await acquireDirLock(controlLockDirPath);
|
|
445
|
+
try {
|
|
446
|
+
releaseWriteLock = await tryAcquireDirLock(writeLockDirPath);
|
|
447
|
+
} finally {
|
|
448
|
+
await releaseControlLock();
|
|
449
|
+
}
|
|
450
|
+
if (releaseWriteLock) {
|
|
451
|
+
return releaseWriteLock;
|
|
452
|
+
}
|
|
453
|
+
await sleep2(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function isPersistentProfileWriteLocked(targetUserDataDir) {
|
|
457
|
+
return await isDirLockHeld(
|
|
458
|
+
buildPersistentProfileWriteLockDirPath(targetUserDataDir)
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
function buildPersistentProfileWriteLockDirPath(targetUserDataDir) {
|
|
462
|
+
return join2(dirname2(targetUserDataDir), `${basename(targetUserDataDir)}.lock`);
|
|
463
|
+
}
|
|
464
|
+
function buildPersistentProfileControlLockDirPath(targetUserDataDir) {
|
|
465
|
+
return join2(
|
|
466
|
+
dirname2(targetUserDataDir),
|
|
467
|
+
`${basename(targetUserDataDir)}.control.lock`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
async function sleep2(ms) {
|
|
471
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/browser/shared-real-browser-session-state.ts
|
|
475
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
476
|
+
import { existsSync as existsSync2 } from "fs";
|
|
477
|
+
import { mkdir as mkdir2, readFile as readFile3, readdir, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
478
|
+
import { basename as basename2, dirname as dirname3, join as join3 } from "path";
|
|
479
|
+
var SHARED_SESSION_METADATA_FILE = "session.json";
|
|
480
|
+
var SHARED_SESSION_CLIENTS_DIR = "clients";
|
|
481
|
+
var SHARED_SESSION_RETRY_DELAY_MS = 50;
|
|
482
|
+
var SHARED_SESSION_METADATA_TEMP_FILE_PREFIX = `${SHARED_SESSION_METADATA_FILE}.`;
|
|
483
|
+
var SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX = ".tmp";
|
|
484
|
+
function buildSharedSessionDirPath(persistentUserDataDir) {
|
|
485
|
+
return join3(
|
|
486
|
+
dirname3(persistentUserDataDir),
|
|
487
|
+
`${basename2(persistentUserDataDir)}.session`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function buildSharedSessionLockPath(persistentUserDataDir) {
|
|
491
|
+
return `${buildSharedSessionDirPath(persistentUserDataDir)}.lock`;
|
|
492
|
+
}
|
|
493
|
+
function buildSharedSessionClientsDirPath(persistentUserDataDir) {
|
|
494
|
+
return join3(
|
|
495
|
+
buildSharedSessionDirPath(persistentUserDataDir),
|
|
496
|
+
SHARED_SESSION_CLIENTS_DIR
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function buildSharedSessionClientPath(persistentUserDataDir, clientId) {
|
|
500
|
+
return join3(
|
|
501
|
+
buildSharedSessionClientsDirPath(persistentUserDataDir),
|
|
502
|
+
`${clientId}.json`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
async function readSharedSessionMetadata(persistentUserDataDir) {
|
|
506
|
+
return (await readSharedSessionMetadataRecord(persistentUserDataDir)).metadata;
|
|
507
|
+
}
|
|
508
|
+
async function writeSharedSessionMetadata(persistentUserDataDir, metadata) {
|
|
509
|
+
const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
|
|
510
|
+
const metadataPath = buildSharedSessionMetadataPath(persistentUserDataDir);
|
|
511
|
+
const tempPath = buildSharedSessionMetadataTempPath(sessionDirPath);
|
|
512
|
+
await mkdir2(sessionDirPath, { recursive: true });
|
|
513
|
+
try {
|
|
514
|
+
await writeFile2(tempPath, JSON.stringify(metadata, null, 2));
|
|
515
|
+
await rename2(tempPath, metadataPath);
|
|
516
|
+
} finally {
|
|
517
|
+
await rm2(tempPath, { force: true }).catch(() => void 0);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function hasLiveSharedRealBrowserSession(persistentUserDataDir) {
|
|
521
|
+
const sessionDirPath = buildSharedSessionDirPath(persistentUserDataDir);
|
|
522
|
+
const metadataRecord = await readSharedSessionMetadataRecord(
|
|
523
|
+
persistentUserDataDir
|
|
524
|
+
);
|
|
525
|
+
if (!metadataRecord.exists) {
|
|
526
|
+
return await hasLiveSharedSessionPublisherOrClients(sessionDirPath);
|
|
527
|
+
}
|
|
528
|
+
if (!metadataRecord.metadata) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if (await getProcessLiveness(metadataRecord.metadata.browserOwner) === "dead") {
|
|
532
|
+
await rm2(sessionDirPath, {
|
|
533
|
+
force: true,
|
|
534
|
+
recursive: true
|
|
535
|
+
}).catch(() => void 0);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
async function waitForSharedRealBrowserSessionToDrain(persistentUserDataDir) {
|
|
541
|
+
while (true) {
|
|
542
|
+
if (!await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
await sleep3(SHARED_SESSION_RETRY_DELAY_MS);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function readSharedSessionMetadataRecord(persistentUserDataDir) {
|
|
549
|
+
try {
|
|
550
|
+
const raw = await readFile3(
|
|
551
|
+
buildSharedSessionMetadataPath(persistentUserDataDir),
|
|
552
|
+
"utf8"
|
|
553
|
+
);
|
|
554
|
+
return {
|
|
555
|
+
exists: true,
|
|
556
|
+
metadata: parseSharedSessionMetadata(JSON.parse(raw))
|
|
557
|
+
};
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return {
|
|
560
|
+
exists: getErrorCode2(error) !== "ENOENT",
|
|
561
|
+
metadata: null
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function hasLiveSharedSessionPublisherOrClients(sessionDirPath) {
|
|
566
|
+
if (!existsSync2(sessionDirPath)) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
let entries;
|
|
570
|
+
try {
|
|
571
|
+
entries = await readDirNames(sessionDirPath);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return getErrorCode2(error) !== "ENOENT";
|
|
574
|
+
}
|
|
575
|
+
let hasUnknownEntries = false;
|
|
576
|
+
for (const entry of entries) {
|
|
577
|
+
if (entry === SHARED_SESSION_METADATA_FILE) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
if (entry === SHARED_SESSION_CLIENTS_DIR) {
|
|
581
|
+
if (await hasDirectoryEntries(join3(sessionDirPath, entry))) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const owner = parseSharedSessionMetadataTempOwner(entry);
|
|
587
|
+
if (!owner) {
|
|
588
|
+
if (isSharedSessionMetadataTempFile(entry)) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
hasUnknownEntries = true;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (await getProcessLiveness(owner) !== "dead") {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (hasUnknownEntries) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
await rm2(sessionDirPath, {
|
|
602
|
+
force: true,
|
|
603
|
+
recursive: true
|
|
604
|
+
}).catch(() => void 0);
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
function buildSharedSessionMetadataPath(persistentUserDataDir) {
|
|
608
|
+
return join3(
|
|
609
|
+
buildSharedSessionDirPath(persistentUserDataDir),
|
|
610
|
+
SHARED_SESSION_METADATA_FILE
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
function buildSharedSessionMetadataTempPath(sessionDirPath) {
|
|
614
|
+
return join3(
|
|
615
|
+
sessionDirPath,
|
|
616
|
+
[
|
|
617
|
+
SHARED_SESSION_METADATA_FILE,
|
|
618
|
+
CURRENT_PROCESS_OWNER.pid,
|
|
619
|
+
CURRENT_PROCESS_OWNER.processStartedAtMs,
|
|
620
|
+
randomUUID2(),
|
|
621
|
+
"tmp"
|
|
622
|
+
].join(".")
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
function parseSharedSessionMetadata(value) {
|
|
626
|
+
if (!value || typeof value !== "object") {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
const parsed = value;
|
|
630
|
+
const browserOwner = parseProcessOwner(parsed.browserOwner);
|
|
631
|
+
const stateOwner = parseProcessOwner(parsed.stateOwner);
|
|
632
|
+
const state = parsed.state === "launching" || parsed.state === "ready" || parsed.state === "closing" ? parsed.state : null;
|
|
633
|
+
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) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
browserOwner,
|
|
638
|
+
createdAt: parsed.createdAt,
|
|
639
|
+
debugPort: parsed.debugPort,
|
|
640
|
+
executablePath: parsed.executablePath,
|
|
641
|
+
headless: parsed.headless,
|
|
642
|
+
persistentUserDataDir: parsed.persistentUserDataDir,
|
|
643
|
+
profileDirectory: parsed.profileDirectory,
|
|
644
|
+
sessionId: parsed.sessionId,
|
|
645
|
+
state,
|
|
646
|
+
stateOwner
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function parseSharedSessionMetadataTempOwner(entryName) {
|
|
650
|
+
if (!isSharedSessionMetadataTempFile(entryName)) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
const segments = entryName.split(".");
|
|
654
|
+
if (segments.length < 5) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
return parseProcessOwner({
|
|
658
|
+
pid: Number.parseInt(segments[2] ?? "", 10),
|
|
659
|
+
processStartedAtMs: Number.parseInt(segments[3] ?? "", 10)
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function isSharedSessionMetadataTempFile(entryName) {
|
|
663
|
+
return entryName.startsWith(SHARED_SESSION_METADATA_TEMP_FILE_PREFIX) && entryName.endsWith(SHARED_SESSION_METADATA_TEMP_FILE_SUFFIX);
|
|
664
|
+
}
|
|
665
|
+
function getErrorCode2(error) {
|
|
666
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
667
|
+
}
|
|
668
|
+
async function hasDirectoryEntries(dirPath) {
|
|
669
|
+
try {
|
|
670
|
+
return (await readDirNames(dirPath)).length > 0;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
return getErrorCode2(error) !== "ENOENT";
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function readDirNames(dirPath) {
|
|
676
|
+
return await readdir(dirPath, { encoding: "utf8" });
|
|
677
|
+
}
|
|
678
|
+
async function sleep3(ms) {
|
|
679
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/browser/persistent-profile.ts
|
|
683
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
684
|
+
var OPENSTEER_META_FILE = ".opensteer-meta.json";
|
|
685
|
+
var OPENSTEER_RUNTIME_META_FILE = ".opensteer-runtime.json";
|
|
686
|
+
var OPENSTEER_RUNTIME_CREATING_FILE = ".opensteer-runtime-creating.json";
|
|
687
|
+
var PROCESS_LIST_MAX_BUFFER_BYTES2 = 16 * 1024 * 1024;
|
|
688
|
+
var PS_COMMAND_ENV2 = { ...process.env, LC_ALL: "C" };
|
|
689
|
+
var CHROME_SINGLETON_ENTRIES = /* @__PURE__ */ new Set([
|
|
690
|
+
"SingletonCookie",
|
|
691
|
+
"SingletonLock",
|
|
692
|
+
"SingletonSocket",
|
|
693
|
+
"DevToolsActivePort",
|
|
694
|
+
"lockfile"
|
|
695
|
+
]);
|
|
696
|
+
var COPY_SKIP_ENTRIES = /* @__PURE__ */ new Set([
|
|
697
|
+
...CHROME_SINGLETON_ENTRIES,
|
|
698
|
+
OPENSTEER_META_FILE,
|
|
699
|
+
OPENSTEER_RUNTIME_META_FILE,
|
|
700
|
+
OPENSTEER_RUNTIME_CREATING_FILE
|
|
701
|
+
]);
|
|
702
|
+
var SKIPPED_ROOT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
703
|
+
"Crash Reports",
|
|
704
|
+
"Crashpad",
|
|
705
|
+
"BrowserMetrics",
|
|
706
|
+
"GrShaderCache",
|
|
707
|
+
"ShaderCache",
|
|
708
|
+
"GraphiteDawnCache",
|
|
709
|
+
"component_crx_cache",
|
|
710
|
+
"Crowd Deny",
|
|
711
|
+
"hyphen-data",
|
|
712
|
+
"OnDeviceHeadSuggestModel",
|
|
713
|
+
"OptimizationGuidePredictionModels",
|
|
714
|
+
"Segmentation Platform",
|
|
715
|
+
"SmartCardDeviceNames",
|
|
716
|
+
"WidevineCdm",
|
|
717
|
+
"pnacl"
|
|
718
|
+
]);
|
|
719
|
+
async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
|
|
720
|
+
const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
|
|
721
|
+
const targetUserDataDir = join4(
|
|
722
|
+
expandHome(profilesRootDir),
|
|
723
|
+
buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
|
|
724
|
+
);
|
|
725
|
+
const sourceProfileDir = join4(resolvedSourceUserDataDir, profileDirectory);
|
|
726
|
+
const metadata = buildPersistentProfileMetadata(
|
|
727
|
+
resolvedSourceUserDataDir,
|
|
728
|
+
profileDirectory
|
|
729
|
+
);
|
|
730
|
+
await mkdir3(dirname4(targetUserDataDir), { recursive: true });
|
|
731
|
+
if (await isHealthyPersistentProfile(
|
|
732
|
+
targetUserDataDir,
|
|
733
|
+
resolvedSourceUserDataDir,
|
|
734
|
+
profileDirectory
|
|
735
|
+
) && !await isPersistentProfileWriteLocked(targetUserDataDir)) {
|
|
736
|
+
return {
|
|
737
|
+
created: false,
|
|
738
|
+
userDataDir: targetUserDataDir
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return await withPersistentProfileWriteAccess(targetUserDataDir, async () => {
|
|
742
|
+
await recoverPersistentProfileBackup(targetUserDataDir);
|
|
743
|
+
await cleanOrphanedOwnedDirs(
|
|
744
|
+
dirname4(targetUserDataDir),
|
|
745
|
+
buildPersistentProfileTempDirNamePrefix(targetUserDataDir)
|
|
746
|
+
);
|
|
747
|
+
if (!existsSync3(sourceProfileDir)) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const created = await createPersistentProfileClone(
|
|
753
|
+
resolvedSourceUserDataDir,
|
|
754
|
+
sourceProfileDir,
|
|
755
|
+
targetUserDataDir,
|
|
756
|
+
profileDirectory,
|
|
757
|
+
metadata
|
|
758
|
+
);
|
|
759
|
+
await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
|
|
760
|
+
return {
|
|
761
|
+
created,
|
|
762
|
+
userDataDir: targetUserDataDir
|
|
763
|
+
};
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async function clearPersistentProfileSingletons(userDataDir) {
|
|
767
|
+
await Promise.all(
|
|
768
|
+
[...CHROME_SINGLETON_ENTRIES].map(
|
|
769
|
+
(entry) => rm3(join4(userDataDir, entry), {
|
|
770
|
+
force: true,
|
|
771
|
+
recursive: true
|
|
772
|
+
}).catch(() => void 0)
|
|
773
|
+
)
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
async function createIsolatedRuntimeProfile(sourceUserDataDir, runtimesRootDir = defaultRuntimeProfilesRootDir()) {
|
|
777
|
+
const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
|
|
778
|
+
const runtimeRootDir = expandHome(runtimesRootDir);
|
|
779
|
+
await mkdir3(runtimeRootDir, { recursive: true });
|
|
780
|
+
const sourceMetadata = await requirePersistentProfileMetadata(
|
|
781
|
+
resolvedSourceUserDataDir
|
|
782
|
+
);
|
|
783
|
+
const runtimeProfile = await reserveRuntimeProfileCreation(
|
|
784
|
+
resolvedSourceUserDataDir,
|
|
785
|
+
runtimeRootDir,
|
|
786
|
+
sourceMetadata.profileDirectory
|
|
787
|
+
);
|
|
788
|
+
try {
|
|
789
|
+
await cleanOrphanedRuntimeProfileDirs(
|
|
790
|
+
runtimeRootDir,
|
|
791
|
+
buildRuntimeProfileDirNamePrefix(resolvedSourceUserDataDir)
|
|
792
|
+
);
|
|
793
|
+
await copyUserDataDirSnapshot(
|
|
794
|
+
resolvedSourceUserDataDir,
|
|
795
|
+
runtimeProfile.userDataDir
|
|
796
|
+
);
|
|
797
|
+
const currentSourceMetadata = await readPersistentProfileMetadata(
|
|
798
|
+
resolvedSourceUserDataDir
|
|
799
|
+
);
|
|
800
|
+
await writeRuntimeProfileMetadata(
|
|
801
|
+
runtimeProfile.userDataDir,
|
|
802
|
+
await buildRuntimeProfileMetadata(
|
|
803
|
+
runtimeProfile.userDataDir,
|
|
804
|
+
resolvedSourceUserDataDir,
|
|
805
|
+
currentSourceMetadata?.profileDirectory ?? sourceMetadata.profileDirectory
|
|
806
|
+
)
|
|
807
|
+
);
|
|
808
|
+
await clearRuntimeProfileCreationState(
|
|
809
|
+
runtimeProfile.userDataDir,
|
|
810
|
+
resolvedSourceUserDataDir
|
|
811
|
+
);
|
|
812
|
+
return {
|
|
813
|
+
persistentUserDataDir: resolvedSourceUserDataDir,
|
|
814
|
+
userDataDir: runtimeProfile.userDataDir
|
|
815
|
+
};
|
|
816
|
+
} catch (error) {
|
|
817
|
+
await clearRuntimeProfileCreationState(
|
|
818
|
+
runtimeProfile.userDataDir,
|
|
819
|
+
resolvedSourceUserDataDir
|
|
820
|
+
);
|
|
821
|
+
await rm3(runtimeProfile.userDataDir, {
|
|
822
|
+
recursive: true,
|
|
823
|
+
force: true
|
|
824
|
+
}).catch(() => void 0);
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function persistIsolatedRuntimeProfile(runtimeUserDataDir, persistentUserDataDir) {
|
|
829
|
+
const resolvedRuntimeUserDataDir = expandHome(runtimeUserDataDir);
|
|
830
|
+
const resolvedPersistentUserDataDir = expandHome(persistentUserDataDir);
|
|
831
|
+
let claimedRuntimeUserDataDir = null;
|
|
832
|
+
try {
|
|
833
|
+
await withPersistentProfileWriteAccess(
|
|
834
|
+
resolvedPersistentUserDataDir,
|
|
835
|
+
async () => {
|
|
836
|
+
await mkdir3(dirname4(resolvedPersistentUserDataDir), {
|
|
837
|
+
recursive: true
|
|
838
|
+
});
|
|
839
|
+
await recoverPersistentProfileBackup(resolvedPersistentUserDataDir);
|
|
840
|
+
await cleanOrphanedOwnedDirs(
|
|
841
|
+
dirname4(resolvedPersistentUserDataDir),
|
|
842
|
+
buildPersistentProfileTempDirNamePrefix(
|
|
843
|
+
resolvedPersistentUserDataDir
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
const metadata = await requirePersistentProfileMetadata(
|
|
847
|
+
resolvedPersistentUserDataDir
|
|
848
|
+
);
|
|
849
|
+
claimedRuntimeUserDataDir = await claimRuntimeProfileForPersist(
|
|
850
|
+
resolvedRuntimeUserDataDir
|
|
851
|
+
);
|
|
852
|
+
const runtimeMetadata = await requireRuntimeProfileMetadata(
|
|
853
|
+
claimedRuntimeUserDataDir,
|
|
854
|
+
resolvedPersistentUserDataDir,
|
|
855
|
+
metadata.profileDirectory,
|
|
856
|
+
resolvedRuntimeUserDataDir
|
|
857
|
+
);
|
|
858
|
+
await mergePersistentProfileSnapshot(
|
|
859
|
+
claimedRuntimeUserDataDir,
|
|
860
|
+
resolvedPersistentUserDataDir,
|
|
861
|
+
metadata,
|
|
862
|
+
runtimeMetadata
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (claimedRuntimeUserDataDir) {
|
|
868
|
+
try {
|
|
869
|
+
await restoreClaimedRuntimeProfile(
|
|
870
|
+
claimedRuntimeUserDataDir,
|
|
871
|
+
resolvedRuntimeUserDataDir
|
|
872
|
+
);
|
|
873
|
+
} catch (restoreError) {
|
|
874
|
+
throw new AggregateError(
|
|
875
|
+
[error, restoreError],
|
|
876
|
+
`Failed to restore runtime profile "${resolvedRuntimeUserDataDir}" after persistence failed.`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
if (claimedRuntimeUserDataDir) {
|
|
883
|
+
await rm3(claimedRuntimeUserDataDir, {
|
|
884
|
+
recursive: true,
|
|
885
|
+
force: true
|
|
886
|
+
}).catch(() => void 0);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function buildPersistentProfileKey(sourceUserDataDir, profileDirectory) {
|
|
890
|
+
const hash = createHash("sha256").update(`${sourceUserDataDir}\0${profileDirectory}`).digest("hex").slice(0, 16);
|
|
891
|
+
const sourceLabel = sanitizePathSegment(basename3(sourceUserDataDir) || "user-data");
|
|
892
|
+
const profileLabel = sanitizePathSegment(profileDirectory || "Default");
|
|
893
|
+
return `${sourceLabel}-${profileLabel}-${hash}`;
|
|
894
|
+
}
|
|
895
|
+
function defaultPersistentProfilesRootDir() {
|
|
896
|
+
return join4(homedir(), ".opensteer", "real-browser-profiles");
|
|
897
|
+
}
|
|
898
|
+
function defaultRuntimeProfilesRootDir() {
|
|
899
|
+
return join4(tmpdir(), "opensteer-real-browser-runtimes");
|
|
900
|
+
}
|
|
901
|
+
function sanitizePathSegment(value) {
|
|
902
|
+
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-");
|
|
903
|
+
return sanitized.replace(/^-|-$/g, "") || "profile";
|
|
904
|
+
}
|
|
905
|
+
function isProfileDirectory(userDataDir, entry) {
|
|
906
|
+
return existsSync3(join4(userDataDir, entry, "Preferences"));
|
|
907
|
+
}
|
|
908
|
+
async function copyUserDataDirSnapshot(sourceUserDataDir, targetUserDataDir) {
|
|
909
|
+
await cp(sourceUserDataDir, targetUserDataDir, {
|
|
910
|
+
recursive: true,
|
|
911
|
+
filter: (candidatePath) => shouldCopyRuntimeSnapshotEntry(sourceUserDataDir, candidatePath)
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function shouldCopyRuntimeSnapshotEntry(userDataDir, candidatePath) {
|
|
915
|
+
const candidateRelativePath = relative(userDataDir, candidatePath);
|
|
916
|
+
if (!candidateRelativePath) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const segments = candidateRelativePath.split(sep).filter(Boolean);
|
|
920
|
+
if (segments.length !== 1) {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
return !COPY_SKIP_ENTRIES.has(segments[0]);
|
|
924
|
+
}
|
|
925
|
+
async function copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, targetProfileDirectory) {
|
|
926
|
+
let entries;
|
|
927
|
+
try {
|
|
928
|
+
entries = await readdir2(sourceUserDataDir);
|
|
929
|
+
} catch {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const copyTasks = [];
|
|
933
|
+
for (const entry of entries) {
|
|
934
|
+
if (COPY_SKIP_ENTRIES.has(entry)) continue;
|
|
935
|
+
if (entry === targetProfileDirectory) continue;
|
|
936
|
+
const sourcePath = join4(sourceUserDataDir, entry);
|
|
937
|
+
const targetPath = join4(targetUserDataDir, entry);
|
|
938
|
+
if (existsSync3(targetPath)) continue;
|
|
939
|
+
let entryStat;
|
|
940
|
+
try {
|
|
941
|
+
entryStat = await stat(sourcePath);
|
|
942
|
+
} catch {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (entryStat.isFile()) {
|
|
946
|
+
copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
|
|
947
|
+
} else if (entryStat.isDirectory()) {
|
|
948
|
+
if (isProfileDirectory(sourceUserDataDir, entry)) continue;
|
|
949
|
+
if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
|
|
950
|
+
copyTasks.push(
|
|
951
|
+
cp(sourcePath, targetPath, { recursive: true }).catch(
|
|
952
|
+
() => void 0
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
await Promise.all(copyTasks);
|
|
958
|
+
}
|
|
959
|
+
async function writePersistentProfileMetadata(userDataDir, metadata) {
|
|
960
|
+
await writeFile3(
|
|
961
|
+
join4(userDataDir, OPENSTEER_META_FILE),
|
|
962
|
+
JSON.stringify(metadata, null, 2)
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
function buildPersistentProfileMetadata(sourceUserDataDir, profileDirectory) {
|
|
966
|
+
return {
|
|
967
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
968
|
+
profileDirectory,
|
|
969
|
+
source: sourceUserDataDir
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async function createPersistentProfileClone(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
|
|
973
|
+
if (existsSync3(targetUserDataDir)) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const tempUserDataDir = await mkdtemp(
|
|
977
|
+
buildPersistentProfileTempDirPrefix(targetUserDataDir)
|
|
978
|
+
);
|
|
979
|
+
let published = false;
|
|
980
|
+
try {
|
|
981
|
+
await materializePersistentProfileSnapshot(
|
|
982
|
+
sourceUserDataDir,
|
|
983
|
+
sourceProfileDir,
|
|
984
|
+
tempUserDataDir,
|
|
985
|
+
profileDirectory,
|
|
986
|
+
metadata
|
|
987
|
+
);
|
|
988
|
+
try {
|
|
989
|
+
await rename3(tempUserDataDir, targetUserDataDir);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (wasDirPublishedByAnotherProcess2(error, targetUserDataDir)) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
published = true;
|
|
997
|
+
return true;
|
|
998
|
+
} finally {
|
|
999
|
+
if (!published) {
|
|
1000
|
+
await rm3(tempUserDataDir, {
|
|
1001
|
+
recursive: true,
|
|
1002
|
+
force: true
|
|
1003
|
+
}).catch(() => void 0);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function materializePersistentProfileSnapshot(sourceUserDataDir, sourceProfileDir, targetUserDataDir, profileDirectory, metadata) {
|
|
1008
|
+
if (!existsSync3(sourceProfileDir)) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`Chrome profile "${profileDirectory}" was not found in "${sourceUserDataDir}".`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
await cp(sourceProfileDir, join4(targetUserDataDir, profileDirectory), {
|
|
1014
|
+
recursive: true
|
|
1015
|
+
});
|
|
1016
|
+
await copyRootLevelEntries(sourceUserDataDir, targetUserDataDir, profileDirectory);
|
|
1017
|
+
await writePersistentProfileMetadata(targetUserDataDir, metadata);
|
|
1018
|
+
}
|
|
1019
|
+
async function mergePersistentProfileSnapshot(runtimeUserDataDir, persistentUserDataDir, metadata, runtimeMetadata) {
|
|
1020
|
+
const tempUserDataDir = await mkdtemp(
|
|
1021
|
+
buildPersistentProfileTempDirPrefix(persistentUserDataDir)
|
|
1022
|
+
);
|
|
1023
|
+
let published = false;
|
|
1024
|
+
try {
|
|
1025
|
+
const baseEntries = deserializeSnapshotManifestEntries(
|
|
1026
|
+
runtimeMetadata.baseEntries
|
|
1027
|
+
);
|
|
1028
|
+
const currentEntries = await collectPersistentSnapshotEntries(
|
|
1029
|
+
persistentUserDataDir,
|
|
1030
|
+
metadata.profileDirectory
|
|
1031
|
+
);
|
|
1032
|
+
const runtimeEntries = await collectPersistentSnapshotEntries(
|
|
1033
|
+
runtimeUserDataDir,
|
|
1034
|
+
metadata.profileDirectory
|
|
1035
|
+
);
|
|
1036
|
+
const mergedEntries = resolveMergedSnapshotEntries(
|
|
1037
|
+
baseEntries,
|
|
1038
|
+
currentEntries,
|
|
1039
|
+
runtimeEntries
|
|
1040
|
+
);
|
|
1041
|
+
await materializeMergedPersistentProfileSnapshot(
|
|
1042
|
+
tempUserDataDir,
|
|
1043
|
+
currentEntries,
|
|
1044
|
+
runtimeEntries,
|
|
1045
|
+
mergedEntries
|
|
1046
|
+
);
|
|
1047
|
+
await writePersistentProfileMetadata(tempUserDataDir, metadata);
|
|
1048
|
+
await replaceProfileDirectory(persistentUserDataDir, tempUserDataDir);
|
|
1049
|
+
published = true;
|
|
1050
|
+
} finally {
|
|
1051
|
+
if (!published) {
|
|
1052
|
+
await rm3(tempUserDataDir, {
|
|
1053
|
+
recursive: true,
|
|
1054
|
+
force: true
|
|
1055
|
+
}).catch(() => void 0);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function buildRuntimeProfileMetadata(runtimeUserDataDir, persistentUserDataDir, profileDirectory) {
|
|
1060
|
+
const baseEntries = profileDirectory ? serializeSnapshotManifestEntries(
|
|
1061
|
+
await collectPersistentSnapshotEntries(
|
|
1062
|
+
runtimeUserDataDir,
|
|
1063
|
+
profileDirectory
|
|
1064
|
+
)
|
|
1065
|
+
) : {};
|
|
1066
|
+
return {
|
|
1067
|
+
baseEntries,
|
|
1068
|
+
creator: CURRENT_PROCESS_OWNER,
|
|
1069
|
+
persistentUserDataDir,
|
|
1070
|
+
profileDirectory
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
async function writeRuntimeProfileMetadata(userDataDir, metadata) {
|
|
1074
|
+
await writeFile3(
|
|
1075
|
+
join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
|
|
1076
|
+
JSON.stringify(metadata, null, 2)
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
async function writeRuntimeProfileCreationMarker(userDataDir, marker) {
|
|
1080
|
+
await writeFile3(
|
|
1081
|
+
join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
|
|
1082
|
+
JSON.stringify(marker, null, 2)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
async function readRuntimeProfileMetadata(userDataDir) {
|
|
1086
|
+
try {
|
|
1087
|
+
const raw = await readFile4(
|
|
1088
|
+
join4(userDataDir, OPENSTEER_RUNTIME_META_FILE),
|
|
1089
|
+
"utf8"
|
|
1090
|
+
);
|
|
1091
|
+
const parsed = JSON.parse(raw);
|
|
1092
|
+
const creator = parseProcessOwner(parsed.creator);
|
|
1093
|
+
const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
|
|
1094
|
+
const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
|
|
1095
|
+
if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || typeof parsed.baseEntries !== "object" || parsed.baseEntries === null || Array.isArray(parsed.baseEntries)) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const baseEntries = deserializeSnapshotManifestEntries(
|
|
1099
|
+
parsed.baseEntries
|
|
1100
|
+
);
|
|
1101
|
+
return {
|
|
1102
|
+
baseEntries: Object.fromEntries(baseEntries),
|
|
1103
|
+
creator,
|
|
1104
|
+
persistentUserDataDir,
|
|
1105
|
+
profileDirectory
|
|
1106
|
+
};
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function readRuntimeProfileCreationMarker(userDataDir) {
|
|
1112
|
+
try {
|
|
1113
|
+
const raw = await readFile4(
|
|
1114
|
+
join4(userDataDir, OPENSTEER_RUNTIME_CREATING_FILE),
|
|
1115
|
+
"utf8"
|
|
1116
|
+
);
|
|
1117
|
+
return parseRuntimeProfileCreationMarker(JSON.parse(raw));
|
|
1118
|
+
} catch {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async function requireRuntimeProfileMetadata(userDataDir, expectedPersistentUserDataDir, expectedProfileDirectory, displayUserDataDir = userDataDir) {
|
|
1123
|
+
const metadata = await readRuntimeProfileMetadata(userDataDir);
|
|
1124
|
+
if (!metadata) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`Runtime profile metadata was not found for "${displayUserDataDir}".`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
if (metadata.profileDirectory !== expectedProfileDirectory) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`Runtime profile "${displayUserDataDir}" was created for profile "${metadata.profileDirectory ?? "unknown"}", expected "${expectedProfileDirectory}".`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
if (metadata.persistentUserDataDir !== expectedPersistentUserDataDir) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`Runtime profile "${displayUserDataDir}" does not belong to persistent profile "${expectedPersistentUserDataDir}".`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return metadata;
|
|
1140
|
+
}
|
|
1141
|
+
async function collectPersistentSnapshotEntries(userDataDir, profileDirectory) {
|
|
1142
|
+
let rootEntries;
|
|
1143
|
+
try {
|
|
1144
|
+
rootEntries = await readdir2(userDataDir, {
|
|
1145
|
+
encoding: "utf8",
|
|
1146
|
+
withFileTypes: true
|
|
1147
|
+
});
|
|
1148
|
+
} catch {
|
|
1149
|
+
return /* @__PURE__ */ new Map();
|
|
1150
|
+
}
|
|
1151
|
+
rootEntries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1152
|
+
const collected = /* @__PURE__ */ new Map();
|
|
1153
|
+
for (const entry of rootEntries) {
|
|
1154
|
+
if (!shouldIncludePersistentRootEntry(
|
|
1155
|
+
userDataDir,
|
|
1156
|
+
profileDirectory,
|
|
1157
|
+
entry.name
|
|
1158
|
+
)) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
await collectSnapshotEntry(
|
|
1162
|
+
join4(userDataDir, entry.name),
|
|
1163
|
+
entry.name,
|
|
1164
|
+
collected
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return collected;
|
|
1168
|
+
}
|
|
1169
|
+
function shouldIncludePersistentRootEntry(userDataDir, profileDirectory, entry) {
|
|
1170
|
+
if (entry === profileDirectory) {
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
if (COPY_SKIP_ENTRIES.has(entry)) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
if (SKIPPED_ROOT_DIRECTORIES.has(entry)) {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
return !isProfileDirectory(userDataDir, entry);
|
|
1180
|
+
}
|
|
1181
|
+
async function collectSnapshotEntry(sourcePath, relativePath, collected) {
|
|
1182
|
+
let entryStat;
|
|
1183
|
+
try {
|
|
1184
|
+
entryStat = await stat(sourcePath);
|
|
1185
|
+
} catch {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (entryStat.isDirectory()) {
|
|
1189
|
+
collected.set(relativePath, {
|
|
1190
|
+
kind: "directory",
|
|
1191
|
+
hash: null,
|
|
1192
|
+
sourcePath
|
|
1193
|
+
});
|
|
1194
|
+
let children;
|
|
1195
|
+
try {
|
|
1196
|
+
children = await readdir2(sourcePath, {
|
|
1197
|
+
encoding: "utf8",
|
|
1198
|
+
withFileTypes: true
|
|
1199
|
+
});
|
|
1200
|
+
} catch {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
children.sort((left, right) => left.name.localeCompare(right.name));
|
|
1204
|
+
for (const child of children) {
|
|
1205
|
+
await collectSnapshotEntry(
|
|
1206
|
+
join4(sourcePath, child.name),
|
|
1207
|
+
join4(relativePath, child.name),
|
|
1208
|
+
collected
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (entryStat.isFile()) {
|
|
1214
|
+
collected.set(relativePath, {
|
|
1215
|
+
kind: "file",
|
|
1216
|
+
hash: await hashSnapshotFile(sourcePath, relativePath),
|
|
1217
|
+
sourcePath
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function serializeSnapshotManifestEntries(entries) {
|
|
1222
|
+
return Object.fromEntries(
|
|
1223
|
+
[...entries.entries()].sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)).map(([relativePath, entry]) => [
|
|
1224
|
+
relativePath,
|
|
1225
|
+
{
|
|
1226
|
+
kind: entry.kind,
|
|
1227
|
+
hash: entry.hash
|
|
1228
|
+
}
|
|
1229
|
+
])
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
function deserializeSnapshotManifestEntries(entries) {
|
|
1233
|
+
const manifestEntries = /* @__PURE__ */ new Map();
|
|
1234
|
+
for (const [relativePath, entry] of Object.entries(entries)) {
|
|
1235
|
+
if (!entry || entry.kind !== "directory" && entry.kind !== "file" || !(entry.hash === null || typeof entry.hash === "string")) {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`Runtime profile metadata for "${relativePath}" is invalid.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
manifestEntries.set(relativePath, {
|
|
1241
|
+
kind: entry.kind,
|
|
1242
|
+
hash: entry.hash
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return manifestEntries;
|
|
1246
|
+
}
|
|
1247
|
+
function resolveMergedSnapshotEntries(baseEntries, currentEntries, runtimeEntries) {
|
|
1248
|
+
const mergedEntries = /* @__PURE__ */ new Map();
|
|
1249
|
+
const relativePaths = /* @__PURE__ */ new Set([
|
|
1250
|
+
...baseEntries.keys(),
|
|
1251
|
+
...currentEntries.keys(),
|
|
1252
|
+
...runtimeEntries.keys()
|
|
1253
|
+
]);
|
|
1254
|
+
for (const relativePath of [...relativePaths].sort(compareSnapshotPaths)) {
|
|
1255
|
+
mergedEntries.set(
|
|
1256
|
+
relativePath,
|
|
1257
|
+
resolveMergedSnapshotEntrySelection(
|
|
1258
|
+
relativePath,
|
|
1259
|
+
baseEntries.get(relativePath) ?? null,
|
|
1260
|
+
currentEntries.get(relativePath) ?? null,
|
|
1261
|
+
runtimeEntries.get(relativePath) ?? null
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
return mergedEntries;
|
|
1266
|
+
}
|
|
1267
|
+
function resolveMergedSnapshotEntrySelection(relativePath, baseEntry, currentEntry, runtimeEntry) {
|
|
1268
|
+
if (snapshotEntriesEqual(runtimeEntry, baseEntry)) {
|
|
1269
|
+
return currentEntry ? "current" : null;
|
|
1270
|
+
}
|
|
1271
|
+
if (snapshotEntriesEqual(currentEntry, baseEntry)) {
|
|
1272
|
+
return runtimeEntry ? "runtime" : null;
|
|
1273
|
+
}
|
|
1274
|
+
if (!baseEntry) {
|
|
1275
|
+
if (!currentEntry) {
|
|
1276
|
+
return runtimeEntry ? "runtime" : null;
|
|
1277
|
+
}
|
|
1278
|
+
if (!runtimeEntry) {
|
|
1279
|
+
return "current";
|
|
1280
|
+
}
|
|
1281
|
+
if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
|
|
1282
|
+
return "current";
|
|
1283
|
+
}
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (!currentEntry && !runtimeEntry) {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
if (snapshotEntriesEqual(currentEntry, runtimeEntry)) {
|
|
1292
|
+
return currentEntry ? "current" : null;
|
|
1293
|
+
}
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Concurrent runtime updates changed "${relativePath}" differently; refusing to overwrite the persistent profile.`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
function snapshotEntriesEqual(left, right) {
|
|
1299
|
+
if (!left || !right) {
|
|
1300
|
+
return left === right;
|
|
1301
|
+
}
|
|
1302
|
+
return left.kind === right.kind && left.hash === right.hash;
|
|
1303
|
+
}
|
|
1304
|
+
async function materializeMergedPersistentProfileSnapshot(targetUserDataDir, currentEntries, runtimeEntries, mergedEntries) {
|
|
1305
|
+
const selectedEntries = [...mergedEntries.entries()].filter(([, selection]) => selection !== null).sort(
|
|
1306
|
+
([leftPath], [rightPath]) => compareSnapshotPaths(leftPath, rightPath)
|
|
1307
|
+
);
|
|
1308
|
+
for (const [relativePath, selection] of selectedEntries) {
|
|
1309
|
+
const entry = (selection === "current" ? currentEntries.get(relativePath) : runtimeEntries.get(relativePath)) ?? null;
|
|
1310
|
+
if (!entry) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const targetPath = join4(targetUserDataDir, relativePath);
|
|
1314
|
+
if (entry.kind === "directory") {
|
|
1315
|
+
await mkdir3(targetPath, { recursive: true });
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
await mkdir3(dirname4(targetPath), { recursive: true });
|
|
1319
|
+
await copyFile(entry.sourcePath, targetPath);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function compareSnapshotPaths(left, right) {
|
|
1323
|
+
const leftDepth = left.split(sep).length;
|
|
1324
|
+
const rightDepth = right.split(sep).length;
|
|
1325
|
+
if (leftDepth !== rightDepth) {
|
|
1326
|
+
return leftDepth - rightDepth;
|
|
1327
|
+
}
|
|
1328
|
+
return left.localeCompare(right);
|
|
1329
|
+
}
|
|
1330
|
+
async function hashSnapshotFile(filePath, relativePath) {
|
|
1331
|
+
const normalizedJson = await readNormalizedSnapshotJson(filePath, relativePath);
|
|
1332
|
+
if (normalizedJson !== null) {
|
|
1333
|
+
return createHash("sha256").update(JSON.stringify(normalizedJson)).digest("hex");
|
|
1334
|
+
}
|
|
1335
|
+
return await hashFile(filePath);
|
|
1336
|
+
}
|
|
1337
|
+
async function readNormalizedSnapshotJson(filePath, relativePath) {
|
|
1338
|
+
const normalizer = SNAPSHOT_JSON_NORMALIZERS.get(relativePath);
|
|
1339
|
+
if (!normalizer) {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const parsed = JSON.parse(await readFile4(filePath, "utf8"));
|
|
1344
|
+
return normalizer(parsed);
|
|
1345
|
+
} catch {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
var SNAPSHOT_JSON_NORMALIZERS = /* @__PURE__ */ new Map([["Local State", normalizeLocalStateSnapshotJson]]);
|
|
1350
|
+
function normalizeLocalStateSnapshotJson(value) {
|
|
1351
|
+
if (!isJsonRecord(value)) {
|
|
1352
|
+
return value;
|
|
1353
|
+
}
|
|
1354
|
+
const { user_experience_metrics: _ignored, ...rest } = value;
|
|
1355
|
+
return rest;
|
|
1356
|
+
}
|
|
1357
|
+
function isJsonRecord(value) {
|
|
1358
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1359
|
+
}
|
|
1360
|
+
async function hashFile(filePath) {
|
|
1361
|
+
return new Promise((resolve, reject) => {
|
|
1362
|
+
const hash = createHash("sha256");
|
|
1363
|
+
const stream = createReadStream(filePath);
|
|
1364
|
+
stream.on("data", (chunk) => {
|
|
1365
|
+
hash.update(chunk);
|
|
1366
|
+
});
|
|
1367
|
+
stream.on("error", reject);
|
|
1368
|
+
stream.on("end", () => {
|
|
1369
|
+
resolve(hash.digest("hex"));
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
async function ensurePersistentProfileMetadata(userDataDir, metadata) {
|
|
1374
|
+
if (existsSync3(join4(userDataDir, OPENSTEER_META_FILE))) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
await writePersistentProfileMetadata(userDataDir, metadata);
|
|
1378
|
+
}
|
|
1379
|
+
async function recoverPersistentProfileBackup(targetUserDataDir) {
|
|
1380
|
+
const backupDirPaths = await listPersistentProfileBackupDirs(targetUserDataDir);
|
|
1381
|
+
if (backupDirPaths.length === 0) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (!existsSync3(targetUserDataDir)) {
|
|
1385
|
+
const [latestBackupDirPath, ...staleBackupDirPaths] = backupDirPaths;
|
|
1386
|
+
await rename3(latestBackupDirPath, targetUserDataDir);
|
|
1387
|
+
await Promise.all(
|
|
1388
|
+
staleBackupDirPaths.map(
|
|
1389
|
+
(backupDirPath) => rm3(backupDirPath, {
|
|
1390
|
+
recursive: true,
|
|
1391
|
+
force: true
|
|
1392
|
+
}).catch(() => void 0)
|
|
1393
|
+
)
|
|
1394
|
+
);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
await Promise.all(
|
|
1398
|
+
backupDirPaths.map(
|
|
1399
|
+
(backupDirPath) => rm3(backupDirPath, {
|
|
1400
|
+
recursive: true,
|
|
1401
|
+
force: true
|
|
1402
|
+
}).catch(() => void 0)
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
async function listPersistentProfileBackupDirs(targetUserDataDir) {
|
|
1407
|
+
const profilesDir = dirname4(targetUserDataDir);
|
|
1408
|
+
let entries;
|
|
1409
|
+
try {
|
|
1410
|
+
entries = await readdir2(profilesDir, {
|
|
1411
|
+
encoding: "utf8",
|
|
1412
|
+
withFileTypes: true
|
|
1413
|
+
});
|
|
1414
|
+
} catch {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const backupDirNamePrefix = buildPersistentProfileBackupDirNamePrefix(targetUserDataDir);
|
|
1418
|
+
return entries.filter(
|
|
1419
|
+
(entry) => entry.isDirectory() && entry.name.startsWith(backupDirNamePrefix)
|
|
1420
|
+
).map((entry) => join4(profilesDir, entry.name)).sort((leftPath, rightPath) => rightPath.localeCompare(leftPath));
|
|
1421
|
+
}
|
|
1422
|
+
async function readPersistentProfileMetadata(userDataDir) {
|
|
1423
|
+
try {
|
|
1424
|
+
const raw = await readFile4(join4(userDataDir, OPENSTEER_META_FILE), "utf8");
|
|
1425
|
+
const parsed = JSON.parse(raw);
|
|
1426
|
+
if (typeof parsed.createdAt !== "string" || typeof parsed.profileDirectory !== "string" || typeof parsed.source !== "string") {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
createdAt: parsed.createdAt,
|
|
1431
|
+
profileDirectory: parsed.profileDirectory,
|
|
1432
|
+
source: parsed.source
|
|
1433
|
+
};
|
|
1434
|
+
} catch {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async function requirePersistentProfileMetadata(userDataDir) {
|
|
1439
|
+
const metadata = await readPersistentProfileMetadata(userDataDir);
|
|
1440
|
+
if (!metadata) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`Persistent profile metadata was not found for "${userDataDir}".`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
return metadata;
|
|
1446
|
+
}
|
|
1447
|
+
async function isHealthyPersistentProfile(userDataDir, expectedSourceUserDataDir, expectedProfileDirectory) {
|
|
1448
|
+
if (!existsSync3(userDataDir) || !existsSync3(join4(userDataDir, expectedProfileDirectory))) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
const metadata = await readPersistentProfileMetadata(userDataDir);
|
|
1452
|
+
return metadata?.source === expectedSourceUserDataDir && metadata.profileDirectory === expectedProfileDirectory;
|
|
1453
|
+
}
|
|
1454
|
+
function wasDirPublishedByAnotherProcess2(error, targetDirPath) {
|
|
1455
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
1456
|
+
return existsSync3(targetDirPath) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
|
|
1457
|
+
}
|
|
1458
|
+
async function replaceProfileDirectory(targetUserDataDir, replacementUserDataDir) {
|
|
1459
|
+
if (!existsSync3(targetUserDataDir)) {
|
|
1460
|
+
await rename3(replacementUserDataDir, targetUserDataDir);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
|
|
1464
|
+
let targetMovedToBackup = false;
|
|
1465
|
+
let replacementPublished = false;
|
|
1466
|
+
try {
|
|
1467
|
+
await rename3(targetUserDataDir, backupUserDataDir);
|
|
1468
|
+
targetMovedToBackup = true;
|
|
1469
|
+
await rename3(replacementUserDataDir, targetUserDataDir);
|
|
1470
|
+
replacementPublished = true;
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
if (targetMovedToBackup && !existsSync3(targetUserDataDir)) {
|
|
1473
|
+
await rename3(backupUserDataDir, targetUserDataDir).catch(() => void 0);
|
|
1474
|
+
}
|
|
1475
|
+
throw error;
|
|
1476
|
+
} finally {
|
|
1477
|
+
if (replacementPublished && targetMovedToBackup && existsSync3(backupUserDataDir)) {
|
|
1478
|
+
await rm3(backupUserDataDir, {
|
|
1479
|
+
recursive: true,
|
|
1480
|
+
force: true
|
|
1481
|
+
}).catch(() => void 0);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function withPersistentProfileWriteAccess(targetUserDataDir, action) {
|
|
1486
|
+
const releaseWriteLock = await acquirePersistentProfileWriteLock(
|
|
1487
|
+
targetUserDataDir
|
|
1488
|
+
);
|
|
1489
|
+
try {
|
|
1490
|
+
await waitForRuntimeProfileCreationsToDrain(targetUserDataDir);
|
|
1491
|
+
await waitForSharedRealBrowserSessionToDrain(targetUserDataDir);
|
|
1492
|
+
return await action();
|
|
1493
|
+
} finally {
|
|
1494
|
+
await releaseWriteLock();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
|
|
1498
|
+
return join4(
|
|
1499
|
+
dirname4(targetUserDataDir),
|
|
1500
|
+
`${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-`
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
function buildPersistentProfileTempDirNamePrefix(targetUserDataDir) {
|
|
1504
|
+
return `${basename3(targetUserDataDir)}-tmp-`;
|
|
1505
|
+
}
|
|
1506
|
+
function buildPersistentProfileBackupDirPath(targetUserDataDir) {
|
|
1507
|
+
return join4(
|
|
1508
|
+
dirname4(targetUserDataDir),
|
|
1509
|
+
`${buildPersistentProfileBackupDirNamePrefix(targetUserDataDir)}${Date.now()}-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
function buildPersistentProfileBackupDirNamePrefix(targetUserDataDir) {
|
|
1513
|
+
return `${basename3(targetUserDataDir)}-backup-`;
|
|
1514
|
+
}
|
|
1515
|
+
function buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir) {
|
|
1516
|
+
return join4(
|
|
1517
|
+
dirname4(persistentUserDataDir),
|
|
1518
|
+
`${basename3(persistentUserDataDir)}.creating`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
function buildRuntimeProfileCreationRegistrationPath(persistentUserDataDir, runtimeUserDataDir) {
|
|
1522
|
+
const key = createHash("sha256").update(runtimeUserDataDir).digest("hex").slice(0, 16);
|
|
1523
|
+
return join4(
|
|
1524
|
+
buildRuntimeProfileCreationRegistryDirPath(persistentUserDataDir),
|
|
1525
|
+
`${key}.json`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
function buildRuntimeProfileKey(sourceUserDataDir) {
|
|
1529
|
+
const hash = createHash("sha256").update(sourceUserDataDir).digest("hex").slice(0, 16);
|
|
1530
|
+
return `${sanitizePathSegment(basename3(sourceUserDataDir) || "profile")}-${hash}`;
|
|
1531
|
+
}
|
|
1532
|
+
function buildRuntimeProfileDirNamePrefix(sourceUserDataDir) {
|
|
1533
|
+
return `${buildRuntimeProfileKey(sourceUserDataDir)}-runtime-`;
|
|
1534
|
+
}
|
|
1535
|
+
function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
|
|
1536
|
+
return join4(
|
|
1537
|
+
runtimesRootDir,
|
|
1538
|
+
buildRuntimeProfileDirNamePrefix(sourceUserDataDir)
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
async function reserveRuntimeProfileCreation(persistentUserDataDir, runtimeRootDir, profileDirectory) {
|
|
1542
|
+
while (true) {
|
|
1543
|
+
let runtimeUserDataDir = null;
|
|
1544
|
+
await withPersistentProfileControlLock(
|
|
1545
|
+
persistentUserDataDir,
|
|
1546
|
+
async () => {
|
|
1547
|
+
if (await isPersistentProfileWriteLocked(persistentUserDataDir)) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (await hasLiveSharedRealBrowserSession(persistentUserDataDir)) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const createdRuntimeUserDataDir = await mkdtemp(
|
|
1554
|
+
buildRuntimeProfileDirPrefix(
|
|
1555
|
+
runtimeRootDir,
|
|
1556
|
+
persistentUserDataDir
|
|
1557
|
+
)
|
|
1558
|
+
);
|
|
1559
|
+
runtimeUserDataDir = createdRuntimeUserDataDir;
|
|
1560
|
+
const marker = {
|
|
1561
|
+
creator: CURRENT_PROCESS_OWNER,
|
|
1562
|
+
persistentUserDataDir,
|
|
1563
|
+
profileDirectory,
|
|
1564
|
+
runtimeUserDataDir: createdRuntimeUserDataDir
|
|
1565
|
+
};
|
|
1566
|
+
await writeRuntimeProfileCreationMarker(
|
|
1567
|
+
createdRuntimeUserDataDir,
|
|
1568
|
+
marker
|
|
1569
|
+
);
|
|
1570
|
+
await writeRuntimeProfileCreationRegistration(marker);
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
if (runtimeUserDataDir) {
|
|
1574
|
+
return {
|
|
1575
|
+
persistentUserDataDir,
|
|
1576
|
+
userDataDir: runtimeUserDataDir
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async function clearRuntimeProfileCreationState(runtimeUserDataDir, persistentUserDataDir) {
|
|
1583
|
+
await Promise.all([
|
|
1584
|
+
rm3(join4(runtimeUserDataDir, OPENSTEER_RUNTIME_CREATING_FILE), {
|
|
1585
|
+
force: true
|
|
1586
|
+
}).catch(() => void 0),
|
|
1587
|
+
rm3(
|
|
1588
|
+
buildRuntimeProfileCreationRegistrationPath(
|
|
1589
|
+
persistentUserDataDir,
|
|
1590
|
+
runtimeUserDataDir
|
|
1591
|
+
),
|
|
1592
|
+
{
|
|
1593
|
+
force: true
|
|
1594
|
+
}
|
|
1595
|
+
).catch(() => void 0)
|
|
1596
|
+
]);
|
|
1597
|
+
}
|
|
1598
|
+
async function writeRuntimeProfileCreationRegistration(marker) {
|
|
1599
|
+
const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
|
|
1600
|
+
marker.persistentUserDataDir
|
|
1601
|
+
);
|
|
1602
|
+
await mkdir3(registryDirPath, { recursive: true });
|
|
1603
|
+
await writeFile3(
|
|
1604
|
+
buildRuntimeProfileCreationRegistrationPath(
|
|
1605
|
+
marker.persistentUserDataDir,
|
|
1606
|
+
marker.runtimeUserDataDir
|
|
1607
|
+
),
|
|
1608
|
+
JSON.stringify(marker, null, 2)
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
async function listRuntimeProfileCreationRegistrations(persistentUserDataDir) {
|
|
1612
|
+
const registryDirPath = buildRuntimeProfileCreationRegistryDirPath(
|
|
1613
|
+
persistentUserDataDir
|
|
1614
|
+
);
|
|
1615
|
+
let entries;
|
|
1616
|
+
try {
|
|
1617
|
+
entries = await readdir2(registryDirPath, {
|
|
1618
|
+
encoding: "utf8",
|
|
1619
|
+
withFileTypes: true
|
|
1620
|
+
});
|
|
1621
|
+
} catch {
|
|
1622
|
+
return [];
|
|
1623
|
+
}
|
|
1624
|
+
return await Promise.all(
|
|
1625
|
+
entries.filter((entry) => entry.isFile()).map(async (entry) => {
|
|
1626
|
+
const filePath = join4(registryDirPath, entry.name);
|
|
1627
|
+
return {
|
|
1628
|
+
filePath,
|
|
1629
|
+
marker: await readRuntimeProfileCreationRegistration(filePath)
|
|
1630
|
+
};
|
|
1631
|
+
})
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
async function readRuntimeProfileCreationRegistration(filePath) {
|
|
1635
|
+
try {
|
|
1636
|
+
const raw = await readFile4(filePath, "utf8");
|
|
1637
|
+
return parseRuntimeProfileCreationMarker(JSON.parse(raw));
|
|
1638
|
+
} catch {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
async function cleanOrphanedRuntimeProfileDirs(rootDir, runtimeDirNamePrefix) {
|
|
1643
|
+
let entries;
|
|
1644
|
+
try {
|
|
1645
|
+
entries = await readdir2(rootDir, {
|
|
1646
|
+
encoding: "utf8",
|
|
1647
|
+
withFileTypes: true
|
|
1648
|
+
});
|
|
1649
|
+
} catch {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const liveProcessCommandLines = await listProcessCommandLines();
|
|
1653
|
+
await Promise.all(
|
|
1654
|
+
entries.map(async (entry) => {
|
|
1655
|
+
if (!entry.isDirectory() || !entry.name.startsWith(runtimeDirNamePrefix)) {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const runtimeDirPath = join4(rootDir, entry.name);
|
|
1659
|
+
const creationMarker = await readRuntimeProfileCreationMarker(
|
|
1660
|
+
runtimeDirPath
|
|
1661
|
+
);
|
|
1662
|
+
if (await isRuntimeProfileDirInUse(
|
|
1663
|
+
runtimeDirPath,
|
|
1664
|
+
liveProcessCommandLines
|
|
1665
|
+
)) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
await rm3(runtimeDirPath, {
|
|
1669
|
+
recursive: true,
|
|
1670
|
+
force: true
|
|
1671
|
+
}).catch(() => void 0);
|
|
1672
|
+
if (creationMarker) {
|
|
1673
|
+
await clearRuntimeProfileCreationState(
|
|
1674
|
+
runtimeDirPath,
|
|
1675
|
+
creationMarker.persistentUserDataDir
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
})
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
async function cleanOrphanedOwnedDirs(rootDir, ownedDirNamePrefix) {
|
|
1682
|
+
let entries;
|
|
1683
|
+
try {
|
|
1684
|
+
entries = await readdir2(rootDir, {
|
|
1685
|
+
encoding: "utf8",
|
|
1686
|
+
withFileTypes: true
|
|
1687
|
+
});
|
|
1688
|
+
} catch {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
await Promise.all(
|
|
1692
|
+
entries.map(async (entry) => {
|
|
1693
|
+
if (!entry.isDirectory() || !entry.name.startsWith(ownedDirNamePrefix)) {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
if (await isOwnedDirByLiveProcess(entry.name, ownedDirNamePrefix)) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
await rm3(join4(rootDir, entry.name), {
|
|
1700
|
+
recursive: true,
|
|
1701
|
+
force: true
|
|
1702
|
+
}).catch(() => void 0);
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
async function isOwnedDirByLiveProcess(ownedDirName, ownedDirPrefix) {
|
|
1707
|
+
const owner = parseOwnedDirOwner(ownedDirName, ownedDirPrefix);
|
|
1708
|
+
return owner ? await getProcessLiveness(owner) !== "dead" : false;
|
|
1709
|
+
}
|
|
1710
|
+
async function isRuntimeProfileDirInUse(runtimeDirPath, liveProcessCommandLines) {
|
|
1711
|
+
const creationMarker = await readRuntimeProfileCreationMarker(runtimeDirPath);
|
|
1712
|
+
if (creationMarker && await getProcessLiveness(creationMarker.creator) !== "dead") {
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
const metadata = await readRuntimeProfileMetadata(runtimeDirPath);
|
|
1716
|
+
if (metadata && await getProcessLiveness(metadata.creator) !== "dead") {
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
return liveProcessCommandLines.some(
|
|
1720
|
+
(commandLine) => commandLineIncludesUserDataDir(commandLine, runtimeDirPath)
|
|
400
1721
|
);
|
|
401
1722
|
}
|
|
402
|
-
async function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1723
|
+
async function claimRuntimeProfileForPersist(runtimeUserDataDir) {
|
|
1724
|
+
while (true) {
|
|
1725
|
+
await waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir);
|
|
1726
|
+
const claimedRuntimeUserDataDir = buildClaimedRuntimeProfileDirPath(runtimeUserDataDir);
|
|
1727
|
+
try {
|
|
1728
|
+
await rename3(runtimeUserDataDir, claimedRuntimeUserDataDir);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
const code = getErrorCode3(error);
|
|
1731
|
+
if (code === "ENOENT") {
|
|
1732
|
+
throw new Error(
|
|
1733
|
+
`Runtime profile "${runtimeUserDataDir}" was not found.`
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
if (code === "EACCES" || code === "EBUSY" || code === "EPERM") {
|
|
1737
|
+
await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
throw error;
|
|
412
1741
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return null;
|
|
1742
|
+
if (!await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
|
|
1743
|
+
return claimedRuntimeUserDataDir;
|
|
1744
|
+
}
|
|
1745
|
+
await rename3(
|
|
1746
|
+
claimedRuntimeUserDataDir,
|
|
1747
|
+
runtimeUserDataDir
|
|
1748
|
+
).catch(() => void 0);
|
|
1749
|
+
await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
422
1750
|
}
|
|
423
1751
|
}
|
|
424
|
-
async function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
throw new Error(
|
|
428
|
-
`Runtime profile metadata was not found for "${userDataDir}".`
|
|
429
|
-
);
|
|
1752
|
+
async function restoreClaimedRuntimeProfile(claimedRuntimeUserDataDir, runtimeUserDataDir) {
|
|
1753
|
+
if (!existsSync3(claimedRuntimeUserDataDir)) {
|
|
1754
|
+
return;
|
|
430
1755
|
}
|
|
431
|
-
if (
|
|
1756
|
+
if (existsSync3(runtimeUserDataDir)) {
|
|
432
1757
|
throw new Error(
|
|
433
|
-
`Runtime profile "${
|
|
1758
|
+
`Runtime profile "${runtimeUserDataDir}" was recreated before the failed persist could restore it from "${claimedRuntimeUserDataDir}".`
|
|
434
1759
|
);
|
|
435
1760
|
}
|
|
436
|
-
|
|
1761
|
+
await rename3(claimedRuntimeUserDataDir, runtimeUserDataDir);
|
|
437
1762
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
1763
|
+
function buildClaimedRuntimeProfileDirPath(runtimeUserDataDir) {
|
|
1764
|
+
return join4(
|
|
1765
|
+
dirname4(runtimeUserDataDir),
|
|
1766
|
+
`${basename3(runtimeUserDataDir)}-persisting-${process.pid}-${CURRENT_PROCESS_OWNER.processStartedAtMs}-${randomUUID3()}`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
async function waitForRuntimeProfileProcessesToDrain(runtimeUserDataDir) {
|
|
1770
|
+
while (await hasLiveProcessUsingUserDataDir(runtimeUserDataDir)) {
|
|
1771
|
+
await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
447
1772
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1773
|
+
}
|
|
1774
|
+
async function hasLiveProcessUsingUserDataDir(userDataDir) {
|
|
1775
|
+
const liveProcessCommandLines = await listProcessCommandLines();
|
|
1776
|
+
return liveProcessCommandLines.some(
|
|
1777
|
+
(commandLine) => commandLineIncludesUserDataDir(commandLine, userDataDir)
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
async function hasActiveRuntimeProfileCreations(persistentUserDataDir) {
|
|
1781
|
+
const registrations = await listRuntimeProfileCreationRegistrations(
|
|
1782
|
+
persistentUserDataDir
|
|
1783
|
+
);
|
|
1784
|
+
let hasLiveCreation = false;
|
|
1785
|
+
for (const registration of registrations) {
|
|
1786
|
+
const marker = registration.marker;
|
|
1787
|
+
if (!marker || marker.persistentUserDataDir !== persistentUserDataDir) {
|
|
1788
|
+
await rm3(registration.filePath, {
|
|
1789
|
+
force: true
|
|
1790
|
+
}).catch(() => void 0);
|
|
456
1791
|
continue;
|
|
457
1792
|
}
|
|
458
|
-
await
|
|
459
|
-
|
|
460
|
-
entry.name,
|
|
461
|
-
collected
|
|
1793
|
+
const runtimeMarker = await readRuntimeProfileCreationMarker(
|
|
1794
|
+
marker.runtimeUserDataDir
|
|
462
1795
|
);
|
|
1796
|
+
if (!runtimeMarker || runtimeMarker.persistentUserDataDir !== persistentUserDataDir || runtimeMarker.runtimeUserDataDir !== marker.runtimeUserDataDir) {
|
|
1797
|
+
await clearRuntimeProfileCreationState(
|
|
1798
|
+
marker.runtimeUserDataDir,
|
|
1799
|
+
persistentUserDataDir
|
|
1800
|
+
);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (await getProcessLiveness(runtimeMarker.creator) === "dead") {
|
|
1804
|
+
await clearRuntimeProfileCreationState(
|
|
1805
|
+
marker.runtimeUserDataDir,
|
|
1806
|
+
persistentUserDataDir
|
|
1807
|
+
);
|
|
1808
|
+
await rm3(marker.runtimeUserDataDir, {
|
|
1809
|
+
recursive: true,
|
|
1810
|
+
force: true
|
|
1811
|
+
}).catch(() => void 0);
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
hasLiveCreation = true;
|
|
463
1815
|
}
|
|
464
|
-
return
|
|
1816
|
+
return hasLiveCreation;
|
|
465
1817
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
1818
|
+
async function waitForRuntimeProfileCreationsToDrain(persistentUserDataDir) {
|
|
1819
|
+
while (true) {
|
|
1820
|
+
if (!await hasActiveRuntimeProfileCreations(persistentUserDataDir)) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
await sleep4(PERSISTENT_PROFILE_LOCK_RETRY_DELAY_MS);
|
|
469
1824
|
}
|
|
470
|
-
|
|
471
|
-
|
|
1825
|
+
}
|
|
1826
|
+
function parseRuntimeProfileCreationMarker(value) {
|
|
1827
|
+
if (!value || typeof value !== "object") {
|
|
1828
|
+
return null;
|
|
472
1829
|
}
|
|
473
|
-
|
|
474
|
-
|
|
1830
|
+
const parsed = value;
|
|
1831
|
+
const creator = parseProcessOwner(parsed.creator);
|
|
1832
|
+
const persistentUserDataDir = typeof parsed.persistentUserDataDir === "string" ? parsed.persistentUserDataDir : void 0;
|
|
1833
|
+
const profileDirectory = parsed.profileDirectory === null ? null : typeof parsed.profileDirectory === "string" ? parsed.profileDirectory : void 0;
|
|
1834
|
+
const runtimeUserDataDir = typeof parsed.runtimeUserDataDir === "string" ? parsed.runtimeUserDataDir : void 0;
|
|
1835
|
+
if (!creator || persistentUserDataDir === void 0 || profileDirectory === void 0 || runtimeUserDataDir === void 0) {
|
|
1836
|
+
return null;
|
|
475
1837
|
}
|
|
476
|
-
return
|
|
1838
|
+
return {
|
|
1839
|
+
creator,
|
|
1840
|
+
persistentUserDataDir,
|
|
1841
|
+
profileDirectory,
|
|
1842
|
+
runtimeUserDataDir
|
|
1843
|
+
};
|
|
477
1844
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return;
|
|
1845
|
+
function parseOwnedDirOwner(ownedDirName, ownedDirPrefix) {
|
|
1846
|
+
const remainder = ownedDirName.slice(ownedDirPrefix.length);
|
|
1847
|
+
const firstDashIndex = remainder.indexOf("-");
|
|
1848
|
+
const secondDashIndex = firstDashIndex === -1 ? -1 : remainder.indexOf("-", firstDashIndex + 1);
|
|
1849
|
+
if (firstDashIndex === -1 || secondDashIndex === -1) {
|
|
1850
|
+
return null;
|
|
484
1851
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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;
|
|
1852
|
+
const pid = Number.parseInt(remainder.slice(0, firstDashIndex), 10);
|
|
1853
|
+
const processStartedAtMs = Number.parseInt(
|
|
1854
|
+
remainder.slice(firstDashIndex + 1, secondDashIndex),
|
|
1855
|
+
10
|
|
1856
|
+
);
|
|
1857
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1858
|
+
return null;
|
|
509
1859
|
}
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
kind: "file",
|
|
513
|
-
hash: await hashFile(sourcePath),
|
|
514
|
-
sourcePath
|
|
515
|
-
});
|
|
1860
|
+
if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
|
|
1861
|
+
return null;
|
|
516
1862
|
}
|
|
1863
|
+
return { pid, processStartedAtMs };
|
|
517
1864
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1865
|
+
async function listProcessCommandLines() {
|
|
1866
|
+
if (process.platform === "win32") {
|
|
1867
|
+
return await listWindowsProcessCommandLines();
|
|
1868
|
+
}
|
|
1869
|
+
return await listPsProcessCommandLines();
|
|
1870
|
+
}
|
|
1871
|
+
async function listPsProcessCommandLines() {
|
|
1872
|
+
try {
|
|
1873
|
+
const { stdout } = await execFileAsync2(
|
|
1874
|
+
"ps",
|
|
1875
|
+
["-axww", "-o", "command="],
|
|
522
1876
|
{
|
|
523
|
-
|
|
524
|
-
|
|
1877
|
+
env: PS_COMMAND_ENV2,
|
|
1878
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
|
|
525
1879
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
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
|
-
});
|
|
1880
|
+
);
|
|
1881
|
+
return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1882
|
+
} catch {
|
|
1883
|
+
return [];
|
|
541
1884
|
}
|
|
542
|
-
return manifestEntries;
|
|
543
1885
|
}
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
baseEntries.get(relativePath) ?? null,
|
|
557
|
-
currentEntries.get(relativePath) ?? null,
|
|
558
|
-
runtimeEntries.get(relativePath) ?? null
|
|
559
|
-
)
|
|
1886
|
+
async function listWindowsProcessCommandLines() {
|
|
1887
|
+
const script = [
|
|
1888
|
+
"$processes = Get-CimInstance Win32_Process | Select-Object CommandLine",
|
|
1889
|
+
"$processes | ConvertTo-Json -Compress"
|
|
1890
|
+
].join("; ");
|
|
1891
|
+
try {
|
|
1892
|
+
const { stdout } = await execFileAsync2(
|
|
1893
|
+
"powershell.exe",
|
|
1894
|
+
["-NoLogo", "-NoProfile", "-Command", script],
|
|
1895
|
+
{
|
|
1896
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
|
|
1897
|
+
}
|
|
560
1898
|
);
|
|
1899
|
+
const parsed = JSON.parse(stdout);
|
|
1900
|
+
const records = Array.isArray(parsed) ? parsed : [parsed];
|
|
1901
|
+
return records.map((record) => record?.CommandLine?.trim() ?? "").filter((commandLine) => commandLine.length > 0);
|
|
1902
|
+
} catch {
|
|
1903
|
+
return [];
|
|
561
1904
|
}
|
|
562
|
-
return mergedEntries;
|
|
563
1905
|
}
|
|
564
|
-
function
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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";
|
|
1906
|
+
function commandLineIncludesUserDataDir(commandLine, userDataDir) {
|
|
1907
|
+
const unquoted = `--user-data-dir=${userDataDir}`;
|
|
1908
|
+
const unquotedIndex = commandLine.indexOf(unquoted);
|
|
1909
|
+
if (unquotedIndex !== -1) {
|
|
1910
|
+
const after = commandLine[unquotedIndex + unquoted.length];
|
|
1911
|
+
if (after === void 0 || after === " " || after === " ") {
|
|
1912
|
+
return true;
|
|
580
1913
|
}
|
|
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
1914
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1915
|
+
return [
|
|
1916
|
+
`--user-data-dir="${userDataDir}"`,
|
|
1917
|
+
`--user-data-dir='${userDataDir}'`
|
|
1918
|
+
].some((candidate) => commandLine.includes(candidate));
|
|
594
1919
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
1920
|
+
function getErrorCode3(error) {
|
|
1921
|
+
return typeof error === "object" && error !== null && "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? error.code : void 0;
|
|
1922
|
+
}
|
|
1923
|
+
async function sleep4(ms) {
|
|
1924
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/browser/shared-real-browser-session.ts
|
|
1928
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1929
|
+
import { spawn } from "child_process";
|
|
1930
|
+
import {
|
|
1931
|
+
mkdir as mkdir4,
|
|
1932
|
+
readFile as readFile5,
|
|
1933
|
+
readdir as readdir3,
|
|
1934
|
+
rm as rm4,
|
|
1935
|
+
writeFile as writeFile4
|
|
1936
|
+
} from "fs/promises";
|
|
1937
|
+
import { createServer } from "net";
|
|
1938
|
+
import { join as join5 } from "path";
|
|
1939
|
+
import {
|
|
1940
|
+
chromium
|
|
1941
|
+
} from "playwright";
|
|
1942
|
+
var SHARED_SESSION_RETRY_DELAY_MS2 = 50;
|
|
1943
|
+
async function acquireSharedRealBrowserSession(options) {
|
|
1944
|
+
const reservation = await reserveSharedSessionClient(options);
|
|
1945
|
+
const sessionContext = await attachToSharedSession(reservation, options);
|
|
1946
|
+
let closed = false;
|
|
1947
|
+
return {
|
|
1948
|
+
browser: sessionContext.browser,
|
|
1949
|
+
context: sessionContext.context,
|
|
1950
|
+
page: sessionContext.page,
|
|
1951
|
+
close: async () => {
|
|
1952
|
+
if (closed) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
closed = true;
|
|
1956
|
+
await releaseSharedSessionClient(sessionContext);
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function getOwnedRealBrowserProcessPolicy(platformName = process.platform) {
|
|
1961
|
+
if (platformName === "win32") {
|
|
1962
|
+
return {
|
|
1963
|
+
detached: false,
|
|
1964
|
+
killStrategy: "taskkill",
|
|
1965
|
+
shouldUnref: true
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
if (platformName === "darwin") {
|
|
1969
|
+
return {
|
|
1970
|
+
detached: false,
|
|
1971
|
+
killStrategy: "process",
|
|
1972
|
+
shouldUnref: true
|
|
1973
|
+
};
|
|
598
1974
|
}
|
|
599
|
-
return
|
|
1975
|
+
return {
|
|
1976
|
+
detached: true,
|
|
1977
|
+
killStrategy: "process-group",
|
|
1978
|
+
shouldUnref: true
|
|
1979
|
+
};
|
|
600
1980
|
}
|
|
601
|
-
async function
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1981
|
+
async function reserveSharedSessionClient(options) {
|
|
1982
|
+
while (true) {
|
|
1983
|
+
const outcome = await withPersistentProfileControlLock(
|
|
1984
|
+
options.persistentProfile.userDataDir,
|
|
1985
|
+
async () => {
|
|
1986
|
+
if (await isPersistentProfileWriteLocked(
|
|
1987
|
+
options.persistentProfile.userDataDir
|
|
1988
|
+
)) {
|
|
1989
|
+
return { kind: "wait" };
|
|
1990
|
+
}
|
|
1991
|
+
if (await hasActiveRuntimeProfileCreations(
|
|
1992
|
+
options.persistentProfile.userDataDir
|
|
1993
|
+
)) {
|
|
1994
|
+
return { kind: "wait" };
|
|
1995
|
+
}
|
|
1996
|
+
return await withSharedSessionLock(
|
|
1997
|
+
options.persistentProfile.userDataDir,
|
|
1998
|
+
async () => {
|
|
1999
|
+
const state = await inspectSharedSessionState(options);
|
|
2000
|
+
if (state.kind === "wait") {
|
|
2001
|
+
return { kind: "wait" };
|
|
2002
|
+
}
|
|
2003
|
+
if (state.kind === "ready") {
|
|
2004
|
+
return {
|
|
2005
|
+
kind: "ready",
|
|
2006
|
+
reservation: await registerSharedSessionClient(
|
|
2007
|
+
options.persistentProfile.userDataDir,
|
|
2008
|
+
state.metadata
|
|
2009
|
+
)
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
kind: "launch",
|
|
2014
|
+
reservation: await launchSharedSession(options)
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
);
|
|
2020
|
+
if (outcome.kind === "wait") {
|
|
2021
|
+
await sleep5(SHARED_SESSION_RETRY_DELAY_MS2);
|
|
608
2022
|
continue;
|
|
609
2023
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
2024
|
+
if (outcome.kind === "ready") {
|
|
2025
|
+
return outcome.reservation;
|
|
2026
|
+
}
|
|
2027
|
+
try {
|
|
2028
|
+
await waitForSharedSessionReady(
|
|
2029
|
+
outcome.reservation.metadata,
|
|
2030
|
+
options.timeoutMs
|
|
2031
|
+
);
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
await cleanupFailedSharedSessionLaunch(outcome.reservation);
|
|
2034
|
+
throw error;
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
return await withSharedSessionLock(
|
|
2038
|
+
options.persistentProfile.userDataDir,
|
|
2039
|
+
async () => {
|
|
2040
|
+
const metadata = await readSharedSessionMetadata(
|
|
2041
|
+
options.persistentProfile.userDataDir
|
|
2042
|
+
);
|
|
2043
|
+
if (!metadata || metadata.sessionId !== outcome.reservation.metadata.sessionId || !processOwnersEqual(
|
|
2044
|
+
metadata.browserOwner,
|
|
2045
|
+
outcome.reservation.launchedBrowserOwner
|
|
2046
|
+
)) {
|
|
2047
|
+
throw new Error(
|
|
2048
|
+
"The shared real-browser session changed before launch finalized."
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
const readyMetadata = {
|
|
2052
|
+
...metadata,
|
|
2053
|
+
state: "ready"
|
|
2054
|
+
};
|
|
2055
|
+
await writeSharedSessionMetadata(
|
|
2056
|
+
options.persistentProfile.userDataDir,
|
|
2057
|
+
readyMetadata
|
|
2058
|
+
);
|
|
2059
|
+
return await registerSharedSessionClient(
|
|
2060
|
+
options.persistentProfile.userDataDir,
|
|
2061
|
+
readyMetadata
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
await cleanupFailedSharedSessionLaunch(outcome.reservation);
|
|
2067
|
+
throw error;
|
|
614
2068
|
}
|
|
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
2069
|
}
|
|
625
|
-
return left.localeCompare(right);
|
|
626
2070
|
}
|
|
627
|
-
async function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
2071
|
+
async function attachToSharedSession(reservation, options) {
|
|
2072
|
+
let browser = null;
|
|
2073
|
+
let page = null;
|
|
2074
|
+
try {
|
|
2075
|
+
const browserWsUrl = await resolveCdpWebSocketUrl(
|
|
2076
|
+
buildSharedSessionDiscoveryUrl(reservation.metadata.debugPort),
|
|
2077
|
+
options.timeoutMs
|
|
2078
|
+
);
|
|
2079
|
+
browser = await chromium.connectOverCDP(browserWsUrl, {
|
|
2080
|
+
timeout: options.timeoutMs
|
|
633
2081
|
});
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
2082
|
+
const context = getPrimaryBrowserContext(browser);
|
|
2083
|
+
page = await getSharedSessionPage(context, reservation.reuseExistingPage);
|
|
2084
|
+
if (options.initialUrl) {
|
|
2085
|
+
await page.goto(options.initialUrl, {
|
|
2086
|
+
timeout: options.timeoutMs,
|
|
2087
|
+
waitUntil: "domcontentloaded"
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
browser,
|
|
2092
|
+
clientId: reservation.client.clientId,
|
|
2093
|
+
context,
|
|
2094
|
+
page,
|
|
2095
|
+
persistentUserDataDir: reservation.metadata.persistentUserDataDir,
|
|
2096
|
+
sessionId: reservation.metadata.sessionId
|
|
2097
|
+
};
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
if (page) {
|
|
2100
|
+
await page.close().catch(() => void 0);
|
|
2101
|
+
}
|
|
2102
|
+
if (browser) {
|
|
2103
|
+
await browser.close().catch(() => void 0);
|
|
2104
|
+
}
|
|
2105
|
+
await cleanupFailedSharedSessionAttach({
|
|
2106
|
+
clientId: reservation.client.clientId,
|
|
2107
|
+
persistentUserDataDir: reservation.metadata.persistentUserDataDir,
|
|
2108
|
+
sessionId: reservation.metadata.sessionId
|
|
637
2109
|
});
|
|
638
|
-
|
|
2110
|
+
throw error;
|
|
2111
|
+
}
|
|
639
2112
|
}
|
|
640
|
-
async function
|
|
641
|
-
|
|
2113
|
+
async function releaseSharedSessionClient(context) {
|
|
2114
|
+
const releasePlan = await prepareSharedSessionCloseIfIdle(
|
|
2115
|
+
context.persistentUserDataDir,
|
|
2116
|
+
context.clientId,
|
|
2117
|
+
context.sessionId
|
|
2118
|
+
);
|
|
2119
|
+
if (releasePlan.closeBrowser) {
|
|
2120
|
+
await closeSharedSessionBrowser(
|
|
2121
|
+
context.persistentUserDataDir,
|
|
2122
|
+
releasePlan,
|
|
2123
|
+
context.browser
|
|
2124
|
+
);
|
|
642
2125
|
return;
|
|
643
2126
|
}
|
|
644
|
-
await
|
|
2127
|
+
await context.page.close().catch(() => void 0);
|
|
2128
|
+
await context.browser.close().catch(() => void 0);
|
|
645
2129
|
}
|
|
646
|
-
async function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
2130
|
+
async function inspectSharedSessionState(options) {
|
|
2131
|
+
const persistentUserDataDir = options.persistentProfile.userDataDir;
|
|
2132
|
+
const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
|
|
2133
|
+
const metadata = await readSharedSessionMetadata(persistentUserDataDir);
|
|
2134
|
+
if (!metadata) {
|
|
2135
|
+
if (liveClients.length > 0) {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
`Shared real-browser session metadata for "${persistentUserDataDir}" is missing while clients are still attached.`
|
|
2138
|
+
);
|
|
652
2139
|
}
|
|
2140
|
+
await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
|
|
2141
|
+
force: true,
|
|
2142
|
+
recursive: true
|
|
2143
|
+
}).catch(() => void 0);
|
|
2144
|
+
return { kind: "missing" };
|
|
2145
|
+
}
|
|
2146
|
+
assertSharedSessionCompatibility(metadata, options);
|
|
2147
|
+
const browserState = await getProcessLiveness(metadata.browserOwner);
|
|
2148
|
+
if (browserState === "dead") {
|
|
2149
|
+
await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
|
|
2150
|
+
force: true,
|
|
2151
|
+
recursive: true
|
|
2152
|
+
}).catch(() => void 0);
|
|
2153
|
+
return { kind: "missing" };
|
|
2154
|
+
}
|
|
2155
|
+
if (metadata.state === "ready") {
|
|
653
2156
|
return {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
source: parsed.source
|
|
2157
|
+
kind: "ready",
|
|
2158
|
+
metadata
|
|
657
2159
|
};
|
|
658
|
-
} catch {
|
|
659
|
-
return null;
|
|
660
2160
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
);
|
|
2161
|
+
const stateOwnerState = await getProcessLiveness(metadata.stateOwner);
|
|
2162
|
+
if (stateOwnerState === "dead") {
|
|
2163
|
+
const recoveredMetadata = {
|
|
2164
|
+
...metadata,
|
|
2165
|
+
state: "ready"
|
|
2166
|
+
};
|
|
2167
|
+
await writeSharedSessionMetadata(persistentUserDataDir, recoveredMetadata);
|
|
2168
|
+
return {
|
|
2169
|
+
kind: "ready",
|
|
2170
|
+
metadata: recoveredMetadata
|
|
2171
|
+
};
|
|
668
2172
|
}
|
|
669
|
-
return
|
|
670
|
-
}
|
|
671
|
-
function
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
2173
|
+
return { kind: "wait" };
|
|
2174
|
+
}
|
|
2175
|
+
async function launchSharedSession(options) {
|
|
2176
|
+
const persistentUserDataDir = options.persistentProfile.userDataDir;
|
|
2177
|
+
await clearPersistentProfileSingletons(persistentUserDataDir);
|
|
2178
|
+
const debugPort = await reserveDebugPort();
|
|
2179
|
+
const launchArgs = buildRealBrowserLaunchArgs({
|
|
2180
|
+
debugPort,
|
|
2181
|
+
headless: options.headless,
|
|
2182
|
+
profileDirectory: options.profileDirectory,
|
|
2183
|
+
userDataDir: persistentUserDataDir
|
|
2184
|
+
});
|
|
2185
|
+
const processPolicy = getOwnedRealBrowserProcessPolicy();
|
|
2186
|
+
const processHandle = spawn(options.executablePath, launchArgs, {
|
|
2187
|
+
detached: processPolicy.detached,
|
|
2188
|
+
stdio: "ignore"
|
|
2189
|
+
});
|
|
2190
|
+
if (processPolicy.shouldUnref) {
|
|
2191
|
+
processHandle.unref();
|
|
679
2192
|
}
|
|
680
|
-
const backupUserDataDir = buildPersistentProfileBackupDirPath(targetUserDataDir);
|
|
681
|
-
let targetMovedToBackup = false;
|
|
682
|
-
let replacementPublished = false;
|
|
683
2193
|
try {
|
|
684
|
-
await
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
2194
|
+
const browserOwner = await waitForSpawnedProcessOwner(
|
|
2195
|
+
processHandle.pid,
|
|
2196
|
+
options.timeoutMs
|
|
2197
|
+
);
|
|
2198
|
+
const metadata = {
|
|
2199
|
+
browserOwner,
|
|
2200
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2201
|
+
debugPort,
|
|
2202
|
+
executablePath: options.executablePath,
|
|
2203
|
+
headless: options.headless,
|
|
2204
|
+
persistentUserDataDir,
|
|
2205
|
+
profileDirectory: options.profileDirectory,
|
|
2206
|
+
sessionId: randomUUID4(),
|
|
2207
|
+
state: "launching",
|
|
2208
|
+
stateOwner: CURRENT_PROCESS_OWNER
|
|
2209
|
+
};
|
|
2210
|
+
await writeSharedSessionMetadata(persistentUserDataDir, metadata);
|
|
2211
|
+
return {
|
|
2212
|
+
launchedBrowserOwner: browserOwner,
|
|
2213
|
+
metadata
|
|
2214
|
+
};
|
|
688
2215
|
} catch (error) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
2216
|
+
await killSpawnedBrowserProcess(processHandle);
|
|
2217
|
+
await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
|
|
2218
|
+
force: true,
|
|
2219
|
+
recursive: true
|
|
2220
|
+
}).catch(() => void 0);
|
|
692
2221
|
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
2222
|
}
|
|
701
2223
|
}
|
|
702
|
-
async function
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
2224
|
+
async function cleanupFailedSharedSessionLaunch(reservation) {
|
|
2225
|
+
const shouldPreserveLiveBrowser = await withSharedSessionLock(
|
|
2226
|
+
reservation.metadata.persistentUserDataDir,
|
|
2227
|
+
async () => {
|
|
2228
|
+
const metadata = await readSharedSessionMetadata(
|
|
2229
|
+
reservation.metadata.persistentUserDataDir
|
|
2230
|
+
);
|
|
2231
|
+
if (metadata && metadata.sessionId === reservation.metadata.sessionId && processOwnersEqual(
|
|
2232
|
+
metadata.browserOwner,
|
|
2233
|
+
reservation.launchedBrowserOwner
|
|
2234
|
+
)) {
|
|
2235
|
+
if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
|
|
2236
|
+
const readyMetadata = {
|
|
2237
|
+
...metadata,
|
|
2238
|
+
state: "ready"
|
|
2239
|
+
};
|
|
2240
|
+
await writeSharedSessionMetadata(
|
|
2241
|
+
reservation.metadata.persistentUserDataDir,
|
|
2242
|
+
readyMetadata
|
|
2243
|
+
);
|
|
2244
|
+
return true;
|
|
716
2245
|
}
|
|
2246
|
+
await rm4(
|
|
2247
|
+
buildSharedSessionDirPath(
|
|
2248
|
+
reservation.metadata.persistentUserDataDir
|
|
2249
|
+
),
|
|
2250
|
+
{
|
|
2251
|
+
force: true,
|
|
2252
|
+
recursive: true
|
|
2253
|
+
}
|
|
2254
|
+
).catch(() => void 0);
|
|
717
2255
|
}
|
|
718
|
-
|
|
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;
|
|
2256
|
+
return false;
|
|
727
2257
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return await action();
|
|
732
|
-
} finally {
|
|
733
|
-
await rm(lockDirPath, {
|
|
734
|
-
recursive: true,
|
|
735
|
-
force: true
|
|
736
|
-
}).catch(() => void 0);
|
|
2258
|
+
);
|
|
2259
|
+
if (shouldPreserveLiveBrowser) {
|
|
2260
|
+
return;
|
|
737
2261
|
}
|
|
2262
|
+
await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
|
|
2263
|
+
await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
|
|
738
2264
|
}
|
|
739
|
-
async function
|
|
740
|
-
await
|
|
2265
|
+
async function cleanupFailedSharedSessionAttach(options) {
|
|
2266
|
+
const closePlan = await prepareSharedSessionCloseIfIdle(
|
|
2267
|
+
options.persistentUserDataDir,
|
|
2268
|
+
options.clientId,
|
|
2269
|
+
options.sessionId
|
|
2270
|
+
);
|
|
2271
|
+
if (!closePlan.closeBrowser) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
await closeSharedSessionBrowser(options.persistentUserDataDir, closePlan);
|
|
741
2275
|
}
|
|
742
|
-
async function
|
|
743
|
-
|
|
2276
|
+
async function waitForSharedSessionReady(metadata, timeoutMs) {
|
|
2277
|
+
await resolveCdpWebSocketUrl(
|
|
2278
|
+
buildSharedSessionDiscoveryUrl(metadata.debugPort),
|
|
2279
|
+
timeoutMs
|
|
2280
|
+
);
|
|
744
2281
|
}
|
|
745
|
-
|
|
746
|
-
|
|
2282
|
+
function buildRealBrowserLaunchArgs(options) {
|
|
2283
|
+
const args = [
|
|
2284
|
+
`--user-data-dir=${options.userDataDir}`,
|
|
2285
|
+
`--profile-directory=${options.profileDirectory}`,
|
|
2286
|
+
`--remote-debugging-port=${options.debugPort}`,
|
|
2287
|
+
"--disable-blink-features=AutomationControlled"
|
|
2288
|
+
];
|
|
2289
|
+
if (options.headless) {
|
|
2290
|
+
args.push("--headless=new");
|
|
2291
|
+
}
|
|
2292
|
+
return args;
|
|
747
2293
|
}
|
|
748
|
-
async function
|
|
749
|
-
|
|
2294
|
+
async function requestBrowserShutdown(browser) {
|
|
2295
|
+
let session = null;
|
|
2296
|
+
try {
|
|
2297
|
+
session = await browser.newBrowserCDPSession();
|
|
2298
|
+
await session.send("Browser.close");
|
|
2299
|
+
} catch {
|
|
2300
|
+
} finally {
|
|
2301
|
+
await session?.detach().catch(() => void 0);
|
|
2302
|
+
}
|
|
750
2303
|
}
|
|
751
|
-
async function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
2304
|
+
async function killOwnedBrowserProcess(owner) {
|
|
2305
|
+
if (await getProcessLiveness(owner) === "dead") {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
await killOwnedBrowserProcessByPid(owner.pid);
|
|
755
2309
|
}
|
|
756
|
-
async function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
};
|
|
2310
|
+
async function killSpawnedBrowserProcess(processHandle) {
|
|
2311
|
+
const pid = processHandle.pid;
|
|
2312
|
+
if (!pid || processHandle.exitCode !== null) {
|
|
2313
|
+
return;
|
|
780
2314
|
}
|
|
2315
|
+
await killOwnedBrowserProcessByPid(pid);
|
|
2316
|
+
await waitForPidToExit(pid, 2e3);
|
|
781
2317
|
}
|
|
782
|
-
async function
|
|
783
|
-
|
|
784
|
-
|
|
2318
|
+
async function killOwnedBrowserProcessByPid(pid) {
|
|
2319
|
+
const processPolicy = getOwnedRealBrowserProcessPolicy();
|
|
2320
|
+
if (processPolicy.killStrategy === "taskkill") {
|
|
2321
|
+
await new Promise((resolve) => {
|
|
2322
|
+
const killer = spawn(
|
|
2323
|
+
"taskkill",
|
|
2324
|
+
["/pid", String(pid), "/t", "/f"],
|
|
2325
|
+
{
|
|
2326
|
+
stdio: "ignore"
|
|
2327
|
+
}
|
|
2328
|
+
);
|
|
2329
|
+
killer.on("error", () => resolve());
|
|
2330
|
+
killer.on("exit", () => resolve());
|
|
2331
|
+
});
|
|
2332
|
+
return;
|
|
785
2333
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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);
|
|
2334
|
+
if (processPolicy.killStrategy === "process-group") {
|
|
2335
|
+
try {
|
|
2336
|
+
process.kill(-pid, "SIGKILL");
|
|
2337
|
+
return;
|
|
2338
|
+
} catch {
|
|
807
2339
|
}
|
|
808
2340
|
}
|
|
2341
|
+
try {
|
|
2342
|
+
process.kill(pid, "SIGKILL");
|
|
2343
|
+
} catch {
|
|
2344
|
+
}
|
|
809
2345
|
}
|
|
810
|
-
async function
|
|
811
|
-
const
|
|
812
|
-
while (
|
|
813
|
-
|
|
814
|
-
|
|
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;
|
|
2346
|
+
async function waitForProcessToExit(owner, timeoutMs) {
|
|
2347
|
+
const deadline = Date.now() + timeoutMs;
|
|
2348
|
+
while (Date.now() < deadline) {
|
|
2349
|
+
if (await getProcessLiveness(owner) === "dead") {
|
|
2350
|
+
return;
|
|
846
2351
|
}
|
|
847
|
-
await
|
|
848
|
-
recursive: true,
|
|
849
|
-
force: true
|
|
850
|
-
}).catch(() => void 0);
|
|
2352
|
+
await sleep5(50);
|
|
851
2353
|
}
|
|
852
2354
|
}
|
|
853
|
-
function
|
|
854
|
-
|
|
855
|
-
|
|
2355
|
+
async function waitForPidToExit(pid, timeoutMs) {
|
|
2356
|
+
const deadline = Date.now() + timeoutMs;
|
|
2357
|
+
while (Date.now() < deadline) {
|
|
2358
|
+
if (!isProcessRunning(pid)) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
await sleep5(50);
|
|
856
2362
|
}
|
|
857
|
-
return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
|
|
858
2363
|
}
|
|
859
|
-
async function
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
PROCESS_STARTED_AT_MS
|
|
863
|
-
)) {
|
|
864
|
-
return "live";
|
|
2364
|
+
async function waitForSpawnedProcessOwner(pid, timeoutMs) {
|
|
2365
|
+
if (!pid || pid <= 0) {
|
|
2366
|
+
throw new Error("Chrome did not expose a child process id.");
|
|
865
2367
|
}
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
2368
|
+
const deadline = Date.now() + timeoutMs;
|
|
2369
|
+
while (Date.now() < deadline) {
|
|
2370
|
+
const owner = await readProcessOwner(pid);
|
|
2371
|
+
if (owner) {
|
|
2372
|
+
return owner;
|
|
2373
|
+
}
|
|
2374
|
+
await sleep5(50);
|
|
872
2375
|
}
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
|
|
876
|
-
return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
|
|
877
|
-
}
|
|
878
|
-
function buildPersistentProfileTempDirPrefix(targetUserDataDir) {
|
|
879
|
-
return join(
|
|
880
|
-
dirname(targetUserDataDir),
|
|
881
|
-
`${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`Chrome process ${pid} did not report a stable process start time.`
|
|
882
2378
|
);
|
|
883
2379
|
}
|
|
884
|
-
function
|
|
885
|
-
return
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
return join(
|
|
889
|
-
dirname(targetUserDataDir),
|
|
890
|
-
`${buildPersistentProfileTempDirNamePrefix(targetUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-backup-${Date.now()}`
|
|
2380
|
+
async function withSharedSessionLock(persistentUserDataDir, action) {
|
|
2381
|
+
return await withDirLock(
|
|
2382
|
+
buildSharedSessionLockPath(persistentUserDataDir),
|
|
2383
|
+
action
|
|
891
2384
|
);
|
|
892
2385
|
}
|
|
893
|
-
function
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
function buildRuntimeProfileDirPrefix(runtimesRootDir, sourceUserDataDir) {
|
|
907
|
-
return join(
|
|
908
|
-
runtimesRootDir,
|
|
909
|
-
`${buildRuntimeProfileDirNamePrefix(sourceUserDataDir)}${process.pid}-${PROCESS_STARTED_AT_MS}-`
|
|
2386
|
+
async function registerSharedSessionClient(persistentUserDataDir, metadata) {
|
|
2387
|
+
const liveClients = await listLiveSharedSessionClients(persistentUserDataDir);
|
|
2388
|
+
const client = buildSharedSessionClientRegistration();
|
|
2389
|
+
await mkdir4(buildSharedSessionClientsDirPath(persistentUserDataDir), {
|
|
2390
|
+
recursive: true
|
|
2391
|
+
});
|
|
2392
|
+
await writeFile4(
|
|
2393
|
+
buildSharedSessionClientPath(persistentUserDataDir, client.clientId),
|
|
2394
|
+
JSON.stringify(client, null, 2),
|
|
2395
|
+
{
|
|
2396
|
+
flag: "wx"
|
|
2397
|
+
}
|
|
910
2398
|
);
|
|
2399
|
+
return {
|
|
2400
|
+
client,
|
|
2401
|
+
metadata,
|
|
2402
|
+
reuseExistingPage: liveClients.length === 0
|
|
2403
|
+
};
|
|
911
2404
|
}
|
|
912
|
-
async function
|
|
2405
|
+
async function removeSharedSessionClientRegistration(persistentUserDataDir, clientId) {
|
|
2406
|
+
await rm4(buildSharedSessionClientPath(persistentUserDataDir, clientId), {
|
|
2407
|
+
force: true
|
|
2408
|
+
}).catch(() => void 0);
|
|
2409
|
+
}
|
|
2410
|
+
async function listLiveSharedSessionClients(persistentUserDataDir) {
|
|
2411
|
+
const clientsDirPath = buildSharedSessionClientsDirPath(persistentUserDataDir);
|
|
913
2412
|
let entries;
|
|
914
2413
|
try {
|
|
915
|
-
entries = await
|
|
2414
|
+
entries = await readdir3(clientsDirPath, {
|
|
916
2415
|
encoding: "utf8",
|
|
917
2416
|
withFileTypes: true
|
|
918
2417
|
});
|
|
919
2418
|
} catch {
|
|
920
|
-
return;
|
|
2419
|
+
return [];
|
|
921
2420
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
await
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
return
|
|
2421
|
+
const liveClients = [];
|
|
2422
|
+
for (const entry of entries) {
|
|
2423
|
+
if (!entry.isFile()) {
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
const filePath = join5(clientsDirPath, entry.name);
|
|
2427
|
+
const registration = await readSharedSessionClientRegistration(filePath);
|
|
2428
|
+
if (!registration) {
|
|
2429
|
+
await rm4(filePath, { force: true }).catch(() => void 0);
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
if (await getProcessLiveness(registration.owner) === "dead") {
|
|
2433
|
+
await rm4(filePath, { force: true }).catch(() => void 0);
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
liveClients.push(registration);
|
|
2437
|
+
}
|
|
2438
|
+
return liveClients;
|
|
940
2439
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
2440
|
+
async function readSharedSessionClientRegistration(filePath) {
|
|
2441
|
+
try {
|
|
2442
|
+
const raw = await readFile5(filePath, "utf8");
|
|
2443
|
+
return parseSharedSessionClientRegistration(JSON.parse(raw));
|
|
2444
|
+
} catch {
|
|
946
2445
|
return null;
|
|
947
2446
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
2447
|
+
}
|
|
2448
|
+
function buildSharedSessionClientRegistration() {
|
|
2449
|
+
return {
|
|
2450
|
+
clientId: randomUUID4(),
|
|
2451
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2452
|
+
owner: CURRENT_PROCESS_OWNER
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function parseSharedSessionClientRegistration(value) {
|
|
2456
|
+
if (!value || typeof value !== "object") {
|
|
954
2457
|
return null;
|
|
955
2458
|
}
|
|
956
|
-
|
|
2459
|
+
const parsed = value;
|
|
2460
|
+
const owner = parseProcessOwner(parsed.owner);
|
|
2461
|
+
if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
|
|
957
2462
|
return null;
|
|
958
2463
|
}
|
|
959
|
-
return {
|
|
2464
|
+
return {
|
|
2465
|
+
clientId: parsed.clientId,
|
|
2466
|
+
createdAt: parsed.createdAt,
|
|
2467
|
+
owner
|
|
2468
|
+
};
|
|
960
2469
|
}
|
|
961
|
-
function
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
967
|
-
return code !== "ESRCH";
|
|
2470
|
+
function assertSharedSessionCompatibility(metadata, options) {
|
|
2471
|
+
if (metadata.executablePath !== options.executablePath) {
|
|
2472
|
+
throw new Error(
|
|
2473
|
+
`Chrome profile "${options.profileDirectory}" is already running with executable "${metadata.executablePath}", not "${options.executablePath}".`
|
|
2474
|
+
);
|
|
968
2475
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
2476
|
+
if (metadata.headless !== options.headless) {
|
|
2477
|
+
throw new Error(
|
|
2478
|
+
`Chrome profile "${options.profileDirectory}" is already running with headless=${metadata.headless}, not ${options.headless}.`
|
|
2479
|
+
);
|
|
973
2480
|
}
|
|
974
|
-
|
|
975
|
-
|
|
2481
|
+
}
|
|
2482
|
+
async function prepareSharedSessionCloseIfIdle(persistentUserDataDir, clientId, sessionId) {
|
|
2483
|
+
return await withSharedSessionLock(persistentUserDataDir, async () => {
|
|
2484
|
+
const metadata = await readSharedSessionMetadata(persistentUserDataDir);
|
|
2485
|
+
await removeSharedSessionClientRegistration(
|
|
2486
|
+
persistentUserDataDir,
|
|
2487
|
+
clientId
|
|
2488
|
+
);
|
|
2489
|
+
if (!metadata || metadata.sessionId !== sessionId) {
|
|
2490
|
+
return {
|
|
2491
|
+
closeBrowser: false,
|
|
2492
|
+
sessionId
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
const liveClients = await listLiveSharedSessionClients(
|
|
2496
|
+
persistentUserDataDir
|
|
2497
|
+
);
|
|
2498
|
+
if (liveClients.length > 0) {
|
|
2499
|
+
return {
|
|
2500
|
+
closeBrowser: false,
|
|
2501
|
+
sessionId: metadata.sessionId
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
const closingMetadata = {
|
|
2505
|
+
...metadata,
|
|
2506
|
+
state: "closing",
|
|
2507
|
+
stateOwner: CURRENT_PROCESS_OWNER
|
|
2508
|
+
};
|
|
2509
|
+
await writeSharedSessionMetadata(
|
|
2510
|
+
persistentUserDataDir,
|
|
2511
|
+
closingMetadata
|
|
2512
|
+
);
|
|
2513
|
+
return {
|
|
2514
|
+
browserOwner: closingMetadata.browserOwner,
|
|
2515
|
+
closeBrowser: true,
|
|
2516
|
+
sessionId: closingMetadata.sessionId
|
|
2517
|
+
};
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
async function closeSharedSessionBrowser(persistentUserDataDir, closePlan, browser) {
|
|
2521
|
+
if (browser) {
|
|
2522
|
+
await requestBrowserShutdown(browser);
|
|
2523
|
+
await waitForProcessToExit(closePlan.browserOwner, 1e3);
|
|
976
2524
|
}
|
|
977
|
-
if (
|
|
978
|
-
|
|
2525
|
+
if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
|
|
2526
|
+
await killOwnedBrowserProcess(closePlan.browserOwner);
|
|
2527
|
+
await waitForProcessToExit(closePlan.browserOwner, 2e3);
|
|
979
2528
|
}
|
|
980
|
-
|
|
2529
|
+
await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
|
|
981
2530
|
}
|
|
982
|
-
async function
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
2531
|
+
async function finalizeSharedSessionClose(persistentUserDataDir, sessionId) {
|
|
2532
|
+
await withSharedSessionLock(persistentUserDataDir, async () => {
|
|
2533
|
+
const metadata = await readSharedSessionMetadata(persistentUserDataDir);
|
|
2534
|
+
if (!metadata || metadata.sessionId !== sessionId) {
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
const liveClients = await listLiveSharedSessionClients(
|
|
2538
|
+
persistentUserDataDir
|
|
2539
|
+
);
|
|
2540
|
+
if (liveClients.length > 0) {
|
|
2541
|
+
const readyMetadata = {
|
|
2542
|
+
...metadata,
|
|
2543
|
+
state: "ready"
|
|
2544
|
+
};
|
|
2545
|
+
await writeSharedSessionMetadata(
|
|
2546
|
+
persistentUserDataDir,
|
|
2547
|
+
readyMetadata
|
|
2548
|
+
);
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
if (await getProcessLiveness(metadata.browserOwner) !== "dead") {
|
|
2552
|
+
const readyMetadata = {
|
|
2553
|
+
...metadata,
|
|
2554
|
+
state: "ready"
|
|
2555
|
+
};
|
|
2556
|
+
await writeSharedSessionMetadata(
|
|
2557
|
+
persistentUserDataDir,
|
|
2558
|
+
readyMetadata
|
|
2559
|
+
);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
|
|
2563
|
+
force: true,
|
|
2564
|
+
recursive: true
|
|
2565
|
+
}).catch(() => void 0);
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
function getPrimaryBrowserContext(browser) {
|
|
2569
|
+
const contexts = browser.contexts();
|
|
2570
|
+
if (contexts.length === 0) {
|
|
2571
|
+
throw new Error(
|
|
2572
|
+
"Connection succeeded but no browser contexts were exposed."
|
|
2573
|
+
);
|
|
992
2574
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return null;
|
|
2575
|
+
return contexts[0];
|
|
2576
|
+
}
|
|
2577
|
+
async function getSharedSessionPage(context, reuseExistingPage) {
|
|
2578
|
+
if (reuseExistingPage) {
|
|
2579
|
+
return await getExistingPageOrCreate(context);
|
|
999
2580
|
}
|
|
1000
|
-
return
|
|
1001
|
-
bootTimeMs + startTicks * 1e3 / clockTicksPerSecond
|
|
1002
|
-
);
|
|
2581
|
+
return await context.newPage();
|
|
1003
2582
|
}
|
|
1004
|
-
function
|
|
1005
|
-
const
|
|
1006
|
-
if (
|
|
1007
|
-
return
|
|
2583
|
+
async function getExistingPageOrCreate(context) {
|
|
2584
|
+
const existingPage = context.pages()[0];
|
|
2585
|
+
if (existingPage) {
|
|
2586
|
+
return existingPage;
|
|
1008
2587
|
}
|
|
1009
|
-
|
|
1010
|
-
const startTicks = Number(fields[LINUX_STAT_START_TIME_FIELD_INDEX]);
|
|
1011
|
-
return Number.isFinite(startTicks) && startTicks >= 0 ? startTicks : null;
|
|
2588
|
+
return await context.newPage();
|
|
1012
2589
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
2590
|
+
function buildSharedSessionDiscoveryUrl(debugPort) {
|
|
2591
|
+
return `http://127.0.0.1:${debugPort}`;
|
|
2592
|
+
}
|
|
2593
|
+
async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
|
|
2594
|
+
if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
|
|
2595
|
+
return cdpUrl;
|
|
2596
|
+
}
|
|
2597
|
+
const versionUrl = normalizeDiscoveryUrl(cdpUrl);
|
|
2598
|
+
const deadline = Date.now() + timeoutMs;
|
|
2599
|
+
let lastError = "CDP discovery did not respond.";
|
|
2600
|
+
while (Date.now() < deadline) {
|
|
2601
|
+
const remaining = Math.max(deadline - Date.now(), 1e3);
|
|
2602
|
+
try {
|
|
2603
|
+
const response = await fetch(versionUrl, {
|
|
2604
|
+
signal: AbortSignal.timeout(Math.min(remaining, 5e3))
|
|
2605
|
+
});
|
|
2606
|
+
if (!response.ok) {
|
|
2607
|
+
lastError = `${response.status} ${response.statusText}`;
|
|
2608
|
+
} else {
|
|
2609
|
+
const payload = await response.json();
|
|
2610
|
+
const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
|
|
2611
|
+
if (wsUrl && wsUrl.trim()) {
|
|
2612
|
+
return wsUrl;
|
|
2613
|
+
}
|
|
2614
|
+
lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
|
|
2615
|
+
}
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
1019
2618
|
}
|
|
1020
|
-
|
|
1021
|
-
bootTimeLine.slice("btime ".length),
|
|
1022
|
-
10
|
|
1023
|
-
);
|
|
1024
|
-
return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
|
|
1025
|
-
} catch {
|
|
1026
|
-
return null;
|
|
2619
|
+
await sleep5(100);
|
|
1027
2620
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
|
2621
|
+
throw new Error(
|
|
2622
|
+
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
1039
2623
|
);
|
|
1040
|
-
return await linuxClockTicksPerSecondPromise;
|
|
1041
2624
|
}
|
|
1042
|
-
|
|
2625
|
+
function normalizeDiscoveryUrl(cdpUrl) {
|
|
2626
|
+
let parsed;
|
|
1043
2627
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
{
|
|
2628
|
+
parsed = new URL(cdpUrl);
|
|
2629
|
+
} catch {
|
|
2630
|
+
throw new Error(
|
|
2631
|
+
`Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
|
|
1048
2632
|
);
|
|
1049
|
-
return parsePsStartedAtMs(stdout);
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
return null;
|
|
1052
2633
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const raw = stdout.trim();
|
|
1056
|
-
if (!raw) {
|
|
1057
|
-
return null;
|
|
2634
|
+
if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
|
|
2635
|
+
return parsed;
|
|
1058
2636
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
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]
|
|
2637
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2638
|
+
throw new Error(
|
|
2639
|
+
`Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
|
|
1072
2640
|
);
|
|
1073
|
-
return parsePsStartedAtMs(stdout);
|
|
1074
|
-
} catch (error) {
|
|
1075
|
-
return null;
|
|
1076
2641
|
}
|
|
2642
|
+
const normalized = new URL(parsed.toString());
|
|
2643
|
+
normalized.pathname = "/json/version";
|
|
2644
|
+
normalized.search = "";
|
|
2645
|
+
normalized.hash = "";
|
|
2646
|
+
return normalized;
|
|
1077
2647
|
}
|
|
1078
|
-
function
|
|
1079
|
-
return
|
|
2648
|
+
async function reserveDebugPort() {
|
|
2649
|
+
return await new Promise((resolve, reject) => {
|
|
2650
|
+
const server = createServer();
|
|
2651
|
+
server.unref();
|
|
2652
|
+
server.on("error", reject);
|
|
2653
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2654
|
+
const address = server.address();
|
|
2655
|
+
if (!address || typeof address === "string") {
|
|
2656
|
+
server.close();
|
|
2657
|
+
reject(new Error("Failed to reserve a local debug port."));
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
server.close((error) => {
|
|
2661
|
+
if (error) {
|
|
2662
|
+
reject(error);
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
resolve(address.port);
|
|
2666
|
+
});
|
|
2667
|
+
});
|
|
2668
|
+
});
|
|
1080
2669
|
}
|
|
1081
|
-
async function
|
|
2670
|
+
async function sleep5(ms) {
|
|
1082
2671
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1083
2672
|
}
|
|
1084
2673
|
|
|
1085
2674
|
// src/browser/pool.ts
|
|
1086
|
-
import { spawn } from "child_process";
|
|
1087
|
-
import { rm as rm2 } from "fs/promises";
|
|
1088
|
-
import { createServer } from "net";
|
|
1089
2675
|
import {
|
|
1090
|
-
chromium
|
|
2676
|
+
chromium as chromium2
|
|
1091
2677
|
} from "playwright";
|
|
1092
2678
|
|
|
1093
2679
|
// src/browser/cdp-proxy.ts
|
|
@@ -1507,15 +3093,14 @@ function errorMessage(error) {
|
|
|
1507
3093
|
// src/browser/pool.ts
|
|
1508
3094
|
var BrowserPool = class {
|
|
1509
3095
|
browser = null;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
managedRuntimeProfile = null;
|
|
3096
|
+
activeSessionClose = null;
|
|
3097
|
+
closeInFlight = null;
|
|
1513
3098
|
defaults;
|
|
1514
3099
|
constructor(defaults = {}) {
|
|
1515
3100
|
this.defaults = defaults;
|
|
1516
3101
|
}
|
|
1517
3102
|
async launch(options = {}) {
|
|
1518
|
-
if (this.browser || this.
|
|
3103
|
+
if (this.browser || this.activeSessionClose) {
|
|
1519
3104
|
await this.close();
|
|
1520
3105
|
}
|
|
1521
3106
|
const mode = options.mode ?? this.defaults.mode ?? "chromium";
|
|
@@ -1562,30 +3147,26 @@ var BrowserPool = class {
|
|
|
1562
3147
|
return this.launchSandbox(options);
|
|
1563
3148
|
}
|
|
1564
3149
|
async close() {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
this.
|
|
1571
|
-
this.launchedProcess = null;
|
|
3150
|
+
if (this.closeInFlight) {
|
|
3151
|
+
await this.closeInFlight;
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
const closeOperation = this.closeCurrent();
|
|
3155
|
+
this.closeInFlight = closeOperation;
|
|
1572
3156
|
try {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
3157
|
+
await closeOperation;
|
|
3158
|
+
this.browser = null;
|
|
3159
|
+
this.activeSessionClose = null;
|
|
1576
3160
|
} finally {
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
if (this.managedRuntimeProfile === managedRuntimeProfile) {
|
|
1585
|
-
this.managedRuntimeProfile = null;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
3161
|
+
this.closeInFlight = null;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
async closeCurrent() {
|
|
3165
|
+
if (this.activeSessionClose) {
|
|
3166
|
+
await this.activeSessionClose();
|
|
3167
|
+
return;
|
|
1588
3168
|
}
|
|
3169
|
+
await this.browser?.close().catch(() => void 0);
|
|
1589
3170
|
}
|
|
1590
3171
|
async connectToRunning(cdpUrl, timeout) {
|
|
1591
3172
|
let browser = null;
|
|
@@ -1600,11 +3181,14 @@ var BrowserPool = class {
|
|
|
1600
3181
|
}
|
|
1601
3182
|
cdpProxy = new CDPProxy(browserWsUrl, targetId);
|
|
1602
3183
|
const proxyWsUrl = await cdpProxy.start();
|
|
1603
|
-
browser = await
|
|
3184
|
+
browser = await chromium2.connectOverCDP(proxyWsUrl, {
|
|
1604
3185
|
timeout: timeout ?? 3e4
|
|
1605
3186
|
});
|
|
1606
3187
|
this.browser = browser;
|
|
1607
|
-
this.
|
|
3188
|
+
this.activeSessionClose = async () => {
|
|
3189
|
+
await browser?.close().catch(() => void 0);
|
|
3190
|
+
cdpProxy?.close();
|
|
3191
|
+
};
|
|
1608
3192
|
const { context, page } = await pickBrowserContextAndPage(browser);
|
|
1609
3193
|
return { browser, context, page, isExternal: true };
|
|
1610
3194
|
} catch (error) {
|
|
@@ -1613,7 +3197,7 @@ var BrowserPool = class {
|
|
|
1613
3197
|
}
|
|
1614
3198
|
cdpProxy?.close();
|
|
1615
3199
|
this.browser = null;
|
|
1616
|
-
this.
|
|
3200
|
+
this.activeSessionClose = null;
|
|
1617
3201
|
throw error;
|
|
1618
3202
|
}
|
|
1619
3203
|
}
|
|
@@ -1633,60 +3217,29 @@ var BrowserPool = class {
|
|
|
1633
3217
|
sourceUserDataDir,
|
|
1634
3218
|
profileDirectory
|
|
1635
3219
|
);
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const launchArgs = buildRealBrowserLaunchArgs({
|
|
1646
|
-
userDataDir: runtimeProfile.userDataDir,
|
|
3220
|
+
const sharedSession = await acquireSharedRealBrowserSession({
|
|
3221
|
+
executablePath,
|
|
3222
|
+
headless: resolveLaunchHeadless(
|
|
3223
|
+
"real",
|
|
3224
|
+
options.headless,
|
|
3225
|
+
this.defaults.headless
|
|
3226
|
+
),
|
|
3227
|
+
initialUrl: options.initialUrl,
|
|
3228
|
+
persistentProfile,
|
|
1647
3229
|
profileDirectory,
|
|
1648
|
-
|
|
1649
|
-
headless
|
|
3230
|
+
timeoutMs: options.timeout ?? 3e4
|
|
1650
3231
|
});
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
`http://127.0.0.1:${debugPort}`,
|
|
1660
|
-
options.timeout ?? 3e4
|
|
1661
|
-
);
|
|
1662
|
-
browser = await chromium.connectOverCDP(wsUrl, {
|
|
1663
|
-
timeout: options.timeout ?? 3e4
|
|
1664
|
-
});
|
|
1665
|
-
const { context, page } = await createOwnedBrowserContextAndPage(
|
|
1666
|
-
browser
|
|
1667
|
-
);
|
|
1668
|
-
if (options.initialUrl) {
|
|
1669
|
-
await page.goto(options.initialUrl, {
|
|
1670
|
-
waitUntil: "domcontentloaded",
|
|
1671
|
-
timeout: options.timeout ?? 3e4
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
this.browser = browser;
|
|
1675
|
-
this.launchedProcess = processHandle;
|
|
1676
|
-
this.managedRuntimeProfile = runtimeProfile;
|
|
1677
|
-
return { browser, context, page, isExternal: false };
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
await browser?.close().catch(() => void 0);
|
|
1680
|
-
await killProcessTree(processHandle);
|
|
1681
|
-
await rm2(runtimeProfile.userDataDir, {
|
|
1682
|
-
recursive: true,
|
|
1683
|
-
force: true
|
|
1684
|
-
}).catch(() => void 0);
|
|
1685
|
-
throw error;
|
|
1686
|
-
}
|
|
3232
|
+
this.browser = sharedSession.browser;
|
|
3233
|
+
this.activeSessionClose = sharedSession.close;
|
|
3234
|
+
return {
|
|
3235
|
+
browser: sharedSession.browser,
|
|
3236
|
+
context: sharedSession.context,
|
|
3237
|
+
page: sharedSession.page,
|
|
3238
|
+
isExternal: false
|
|
3239
|
+
};
|
|
1687
3240
|
}
|
|
1688
3241
|
async launchSandbox(options) {
|
|
1689
|
-
const browser = await
|
|
3242
|
+
const browser = await chromium2.launch({
|
|
1690
3243
|
headless: resolveLaunchHeadless(
|
|
1691
3244
|
"chromium",
|
|
1692
3245
|
options.headless,
|
|
@@ -1699,11 +3252,14 @@ var BrowserPool = class {
|
|
|
1699
3252
|
const context = await browser.newContext(options.context || {});
|
|
1700
3253
|
const page = await context.newPage();
|
|
1701
3254
|
this.browser = browser;
|
|
3255
|
+
this.activeSessionClose = async () => {
|
|
3256
|
+
await browser.close().catch(() => void 0);
|
|
3257
|
+
};
|
|
1702
3258
|
return { browser, context, page, isExternal: false };
|
|
1703
3259
|
}
|
|
1704
3260
|
};
|
|
1705
3261
|
async function pickBrowserContextAndPage(browser) {
|
|
1706
|
-
const context =
|
|
3262
|
+
const context = getPrimaryBrowserContext2(browser);
|
|
1707
3263
|
const page = await getAttachedPageOrCreate(context);
|
|
1708
3264
|
return { context, page };
|
|
1709
3265
|
}
|
|
@@ -1716,11 +3272,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
|
|
|
1716
3272
|
}
|
|
1717
3273
|
return mode === "real";
|
|
1718
3274
|
}
|
|
1719
|
-
async function createOwnedBrowserContextAndPage(browser) {
|
|
1720
|
-
const context = getPrimaryBrowserContext(browser);
|
|
1721
|
-
const page = await getExistingPageOrCreate(context);
|
|
1722
|
-
return { context, page };
|
|
1723
|
-
}
|
|
1724
3275
|
async function getAttachedPageOrCreate(context) {
|
|
1725
3276
|
const pages = context.pages();
|
|
1726
3277
|
const inspectablePage = pages.find(
|
|
@@ -1735,14 +3286,7 @@ async function getAttachedPageOrCreate(context) {
|
|
|
1735
3286
|
}
|
|
1736
3287
|
return await context.newPage();
|
|
1737
3288
|
}
|
|
1738
|
-
|
|
1739
|
-
const existingPage = context.pages()[0];
|
|
1740
|
-
if (existingPage) {
|
|
1741
|
-
return existingPage;
|
|
1742
|
-
}
|
|
1743
|
-
return await context.newPage();
|
|
1744
|
-
}
|
|
1745
|
-
function getPrimaryBrowserContext(browser) {
|
|
3289
|
+
function getPrimaryBrowserContext2(browser) {
|
|
1746
3290
|
const contexts = browser.contexts();
|
|
1747
3291
|
if (contexts.length === 0) {
|
|
1748
3292
|
throw new Error(
|
|
@@ -1761,125 +3305,6 @@ function safePageUrl(page) {
|
|
|
1761
3305
|
return "";
|
|
1762
3306
|
}
|
|
1763
3307
|
}
|
|
1764
|
-
function normalizeDiscoveryUrl(cdpUrl) {
|
|
1765
|
-
let parsed;
|
|
1766
|
-
try {
|
|
1767
|
-
parsed = new URL(cdpUrl);
|
|
1768
|
-
} catch {
|
|
1769
|
-
throw new Error(
|
|
1770
|
-
`Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
|
|
1771
|
-
);
|
|
1772
|
-
}
|
|
1773
|
-
if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
|
|
1774
|
-
return parsed;
|
|
1775
|
-
}
|
|
1776
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1777
|
-
throw new Error(
|
|
1778
|
-
`Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
|
|
1779
|
-
);
|
|
1780
|
-
}
|
|
1781
|
-
const normalized = new URL(parsed.toString());
|
|
1782
|
-
normalized.pathname = "/json/version";
|
|
1783
|
-
normalized.search = "";
|
|
1784
|
-
normalized.hash = "";
|
|
1785
|
-
return normalized;
|
|
1786
|
-
}
|
|
1787
|
-
async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
|
|
1788
|
-
if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
|
|
1789
|
-
return cdpUrl;
|
|
1790
|
-
}
|
|
1791
|
-
const versionUrl = normalizeDiscoveryUrl(cdpUrl);
|
|
1792
|
-
const deadline = Date.now() + timeoutMs;
|
|
1793
|
-
let lastError = "CDP discovery did not respond.";
|
|
1794
|
-
while (Date.now() < deadline) {
|
|
1795
|
-
const remaining = Math.max(deadline - Date.now(), 1e3);
|
|
1796
|
-
try {
|
|
1797
|
-
const response = await fetch(versionUrl, {
|
|
1798
|
-
signal: AbortSignal.timeout(Math.min(remaining, 5e3))
|
|
1799
|
-
});
|
|
1800
|
-
if (!response.ok) {
|
|
1801
|
-
lastError = `${response.status} ${response.statusText}`;
|
|
1802
|
-
} else {
|
|
1803
|
-
const payload = await response.json();
|
|
1804
|
-
const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
|
|
1805
|
-
if (wsUrl && wsUrl.trim()) {
|
|
1806
|
-
return wsUrl;
|
|
1807
|
-
}
|
|
1808
|
-
lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
|
|
1809
|
-
}
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
1812
|
-
}
|
|
1813
|
-
await sleep2(100);
|
|
1814
|
-
}
|
|
1815
|
-
throw new Error(
|
|
1816
|
-
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
1817
|
-
);
|
|
1818
|
-
}
|
|
1819
|
-
async function reserveDebugPort() {
|
|
1820
|
-
return await new Promise((resolve, reject) => {
|
|
1821
|
-
const server = createServer();
|
|
1822
|
-
server.unref();
|
|
1823
|
-
server.on("error", reject);
|
|
1824
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1825
|
-
const address = server.address();
|
|
1826
|
-
if (!address || typeof address === "string") {
|
|
1827
|
-
server.close();
|
|
1828
|
-
reject(new Error("Failed to reserve a local debug port."));
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
server.close((error) => {
|
|
1832
|
-
if (error) {
|
|
1833
|
-
reject(error);
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
resolve(address.port);
|
|
1837
|
-
});
|
|
1838
|
-
});
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
function buildRealBrowserLaunchArgs(options) {
|
|
1842
|
-
const args = [
|
|
1843
|
-
`--user-data-dir=${options.userDataDir}`,
|
|
1844
|
-
`--profile-directory=${options.profileDirectory}`,
|
|
1845
|
-
`--remote-debugging-port=${options.debugPort}`,
|
|
1846
|
-
"--disable-blink-features=AutomationControlled"
|
|
1847
|
-
];
|
|
1848
|
-
if (options.headless) {
|
|
1849
|
-
args.push("--headless=new");
|
|
1850
|
-
}
|
|
1851
|
-
return args;
|
|
1852
|
-
}
|
|
1853
|
-
async function killProcessTree(processHandle) {
|
|
1854
|
-
if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
if (process.platform === "win32") {
|
|
1858
|
-
await new Promise((resolve) => {
|
|
1859
|
-
const killer = spawn(
|
|
1860
|
-
"taskkill",
|
|
1861
|
-
["/pid", String(processHandle.pid), "/t", "/f"],
|
|
1862
|
-
{
|
|
1863
|
-
stdio: "ignore"
|
|
1864
|
-
}
|
|
1865
|
-
);
|
|
1866
|
-
killer.on("error", () => resolve());
|
|
1867
|
-
killer.on("exit", () => resolve());
|
|
1868
|
-
});
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
try {
|
|
1872
|
-
process.kill(-processHandle.pid, "SIGKILL");
|
|
1873
|
-
} catch {
|
|
1874
|
-
try {
|
|
1875
|
-
processHandle.kill("SIGKILL");
|
|
1876
|
-
} catch {
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
async function sleep2(ms) {
|
|
1881
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1882
|
-
}
|
|
1883
3308
|
|
|
1884
3309
|
// src/navigation.ts
|
|
1885
3310
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -2259,7 +3684,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
2259
3684
|
TRANSIENT_CONTEXT_RETRY_DELAY_MS,
|
|
2260
3685
|
Math.max(0, deadline - Date.now())
|
|
2261
3686
|
);
|
|
2262
|
-
await
|
|
3687
|
+
await sleep6(retryDelay);
|
|
2263
3688
|
}
|
|
2264
3689
|
}
|
|
2265
3690
|
}
|
|
@@ -2292,7 +3717,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
2292
3717
|
() => ({ kind: "resolved" }),
|
|
2293
3718
|
(error) => ({ kind: "rejected", error })
|
|
2294
3719
|
);
|
|
2295
|
-
const timeoutPromise =
|
|
3720
|
+
const timeoutPromise = sleep6(
|
|
2296
3721
|
timeout + FRAME_EVALUATE_GRACE_MS
|
|
2297
3722
|
).then(() => ({ kind: "timeout" }));
|
|
2298
3723
|
const result = await Promise.race([
|
|
@@ -2434,7 +3859,7 @@ function isIgnorableFrameError(error) {
|
|
|
2434
3859
|
const message = error.message;
|
|
2435
3860
|
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");
|
|
2436
3861
|
}
|
|
2437
|
-
function
|
|
3862
|
+
function sleep6(ms) {
|
|
2438
3863
|
return new Promise((resolve) => {
|
|
2439
3864
|
setTimeout(resolve, ms);
|
|
2440
3865
|
});
|
|
@@ -6686,7 +8111,7 @@ async function closeTab(context, activePage, index) {
|
|
|
6686
8111
|
}
|
|
6687
8112
|
|
|
6688
8113
|
// src/actions/cookies.ts
|
|
6689
|
-
import { readFile as
|
|
8114
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
6690
8115
|
async function getCookies(context, url) {
|
|
6691
8116
|
return context.cookies(url ? [url] : void 0);
|
|
6692
8117
|
}
|
|
@@ -6698,10 +8123,10 @@ async function clearCookies(context) {
|
|
|
6698
8123
|
}
|
|
6699
8124
|
async function exportCookies(context, filePath, url) {
|
|
6700
8125
|
const cookies = await context.cookies(url ? [url] : void 0);
|
|
6701
|
-
await
|
|
8126
|
+
await writeFile5(filePath, JSON.stringify(cookies, null, 2), "utf-8");
|
|
6702
8127
|
}
|
|
6703
8128
|
async function importCookies(context, filePath) {
|
|
6704
|
-
const raw = await
|
|
8129
|
+
const raw = await readFile6(filePath, "utf-8");
|
|
6705
8130
|
const cookies = JSON.parse(raw);
|
|
6706
8131
|
await context.addCookies(cookies);
|
|
6707
8132
|
}
|
|
@@ -8529,7 +9954,7 @@ async function executeAgentAction(page, action) {
|
|
|
8529
9954
|
}
|
|
8530
9955
|
case "wait": {
|
|
8531
9956
|
const ms = numberOr(action.timeMs, action.time_ms, 1e3);
|
|
8532
|
-
await
|
|
9957
|
+
await sleep7(ms);
|
|
8533
9958
|
return;
|
|
8534
9959
|
}
|
|
8535
9960
|
case "goto": {
|
|
@@ -8694,7 +10119,7 @@ async function pressKeyCombo(page, combo) {
|
|
|
8694
10119
|
}
|
|
8695
10120
|
}
|
|
8696
10121
|
}
|
|
8697
|
-
function
|
|
10122
|
+
function sleep7(ms) {
|
|
8698
10123
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
8699
10124
|
}
|
|
8700
10125
|
|
|
@@ -8725,7 +10150,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
8725
10150
|
if (isMutatingAgentAction(action)) {
|
|
8726
10151
|
this.onMutatingAction?.(action);
|
|
8727
10152
|
}
|
|
8728
|
-
await
|
|
10153
|
+
await sleep8(this.config.waitBetweenActionsMs);
|
|
8729
10154
|
});
|
|
8730
10155
|
try {
|
|
8731
10156
|
const result = await this.client.execute({
|
|
@@ -8787,7 +10212,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
8787
10212
|
await this.cursorController.preview({ x, y }, "agent");
|
|
8788
10213
|
}
|
|
8789
10214
|
};
|
|
8790
|
-
function
|
|
10215
|
+
function sleep8(ms) {
|
|
8791
10216
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
8792
10217
|
}
|
|
8793
10218
|
|
|
@@ -9223,7 +10648,7 @@ var CursorController = class {
|
|
|
9223
10648
|
for (const step of motion.points) {
|
|
9224
10649
|
await this.renderer.move(step, this.style);
|
|
9225
10650
|
if (motion.stepDelayMs > 0) {
|
|
9226
|
-
await
|
|
10651
|
+
await sleep9(motion.stepDelayMs);
|
|
9227
10652
|
}
|
|
9228
10653
|
}
|
|
9229
10654
|
if (shouldPulse(intent)) {
|
|
@@ -9381,12 +10806,12 @@ function clamp2(value, min, max) {
|
|
|
9381
10806
|
function shouldPulse(intent) {
|
|
9382
10807
|
return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
|
|
9383
10808
|
}
|
|
9384
|
-
function
|
|
10809
|
+
function sleep9(ms) {
|
|
9385
10810
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9386
10811
|
}
|
|
9387
10812
|
|
|
9388
10813
|
// src/opensteer.ts
|
|
9389
|
-
import { createHash as createHash2, randomUUID as
|
|
10814
|
+
import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
|
|
9390
10815
|
|
|
9391
10816
|
// src/action-wait.ts
|
|
9392
10817
|
var ROBUST_PROFILE = {
|
|
@@ -9581,7 +11006,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
9581
11006
|
this.idleSince = 0;
|
|
9582
11007
|
}
|
|
9583
11008
|
const remaining = Math.max(1, options.deadline - now);
|
|
9584
|
-
await
|
|
11009
|
+
await sleep10(Math.min(NETWORK_POLL_MS, remaining));
|
|
9585
11010
|
}
|
|
9586
11011
|
}
|
|
9587
11012
|
handleRequestStarted = (request) => {
|
|
@@ -9626,7 +11051,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
9626
11051
|
return false;
|
|
9627
11052
|
}
|
|
9628
11053
|
};
|
|
9629
|
-
async function
|
|
11054
|
+
async function sleep10(ms) {
|
|
9630
11055
|
await new Promise((resolve) => {
|
|
9631
11056
|
setTimeout(resolve, ms);
|
|
9632
11057
|
});
|
|
@@ -11406,15 +12831,19 @@ var Opensteer = class _Opensteer {
|
|
|
11406
12831
|
}
|
|
11407
12832
|
return;
|
|
11408
12833
|
}
|
|
12834
|
+
let closedOwnedBrowser = false;
|
|
11409
12835
|
try {
|
|
11410
12836
|
if (this.ownsBrowser) {
|
|
11411
12837
|
await this.pool.close();
|
|
12838
|
+
closedOwnedBrowser = true;
|
|
11412
12839
|
}
|
|
11413
12840
|
} finally {
|
|
11414
12841
|
this.browser = null;
|
|
11415
12842
|
this.pageRef = null;
|
|
11416
12843
|
this.contextRef = null;
|
|
11417
|
-
this.ownsBrowser
|
|
12844
|
+
if (!this.ownsBrowser || closedOwnedBrowser) {
|
|
12845
|
+
this.ownsBrowser = false;
|
|
12846
|
+
}
|
|
11418
12847
|
if (this.cursorController) {
|
|
11419
12848
|
await this.cursorController.dispose().catch(() => void 0);
|
|
11420
12849
|
}
|
|
@@ -13741,7 +15170,7 @@ function normalizeCloudBrowserProfilePreference(value, source) {
|
|
|
13741
15170
|
}
|
|
13742
15171
|
function buildLocalRunId(namespace) {
|
|
13743
15172
|
const normalized = namespace.trim() || "default";
|
|
13744
|
-
return `${normalized}-${Date.now().toString(36)}-${
|
|
15173
|
+
return `${normalized}-${Date.now().toString(36)}-${randomUUID5().slice(0, 8)}`;
|
|
13745
15174
|
}
|
|
13746
15175
|
|
|
13747
15176
|
export {
|
|
@@ -13749,6 +15178,8 @@ export {
|
|
|
13749
15178
|
clearPersistentProfileSingletons,
|
|
13750
15179
|
createIsolatedRuntimeProfile,
|
|
13751
15180
|
persistIsolatedRuntimeProfile,
|
|
15181
|
+
hasActiveRuntimeProfileCreations,
|
|
15182
|
+
getOwnedRealBrowserProcessPolicy,
|
|
13752
15183
|
BrowserPool,
|
|
13753
15184
|
waitForVisualStability,
|
|
13754
15185
|
createEmptyRegistry,
|