opensteer 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JZF2WC7U.js → chunk-HBTSQ2V4.js} +2680 -455
- package/dist/cli/profile.cjs +1996 -536
- package/dist/cli/profile.js +1 -1
- package/dist/cli/server.cjs +1916 -456
- package/dist/cli/server.js +1 -1
- package/dist/index.cjs +2676 -456
- package/dist/index.d.cts +26 -12
- package/dist/index.d.ts +26 -12
- package/dist/index.js +9 -1
- package/package.json +1 -1
|
@@ -25,284 +25,2655 @@ import {
|
|
|
25
25
|
} from "./chunk-3H5RRIMZ.js";
|
|
26
26
|
|
|
27
27
|
// src/browser/persistent-profile.ts
|
|
28
|
-
import { createHash } from "crypto";
|
|
29
|
-
import {
|
|
28
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
29
|
+
import { execFile as execFile2 } from "child_process";
|
|
30
|
+
import { createReadStream, existsSync as existsSync3 } from "fs";
|
|
30
31
|
import {
|
|
31
32
|
cp,
|
|
32
33
|
copyFile,
|
|
33
|
-
mkdir,
|
|
34
|
+
mkdir as mkdir3,
|
|
34
35
|
mkdtemp,
|
|
35
|
-
readdir,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
readdir as readdir2,
|
|
37
|
+
readFile as readFile4,
|
|
38
|
+
rename as rename3,
|
|
39
|
+
rm as rm3,
|
|
38
40
|
stat,
|
|
39
|
-
writeFile
|
|
41
|
+
writeFile as writeFile3
|
|
40
42
|
} from "fs/promises";
|
|
41
|
-
import { homedir } from "os";
|
|
42
|
-
import { basename, dirname, join } from "path";
|
|
43
|
-
|
|
43
|
+
import { homedir, tmpdir } from "os";
|
|
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";
|
|
59
|
+
import { promisify } from "util";
|
|
60
|
+
var execFileAsync = promisify(execFile);
|
|
44
61
|
var PROCESS_STARTED_AT_MS = Math.floor(Date.now() - process.uptime() * 1e3);
|
|
45
62
|
var PROCESS_START_TIME_TOLERANCE_MS = 1e3;
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
var
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"hyphen-data",
|
|
67
|
-
"OnDeviceHeadSuggestModel",
|
|
68
|
-
"OptimizationGuidePredictionModels",
|
|
69
|
-
"Segmentation Platform",
|
|
70
|
-
"SmartCardDeviceNames",
|
|
71
|
-
"WidevineCdm",
|
|
72
|
-
"pnacl"
|
|
73
|
-
]);
|
|
74
|
-
async function getOrCreatePersistentProfile(sourceUserDataDir, profileDirectory, profilesRootDir = defaultPersistentProfilesRootDir()) {
|
|
75
|
-
const resolvedSourceUserDataDir = expandHome(sourceUserDataDir);
|
|
76
|
-
const targetUserDataDir = join(
|
|
77
|
-
expandHome(profilesRootDir),
|
|
78
|
-
buildPersistentProfileKey(resolvedSourceUserDataDir, profileDirectory)
|
|
79
|
-
);
|
|
80
|
-
const sourceProfileDir = join(resolvedSourceUserDataDir, profileDirectory);
|
|
81
|
-
const metadata = buildPersistentProfileMetadata(
|
|
82
|
-
resolvedSourceUserDataDir,
|
|
83
|
-
profileDirectory
|
|
84
|
-
);
|
|
85
|
-
await mkdir(dirname(targetUserDataDir), { recursive: true });
|
|
86
|
-
await cleanOrphanedTempDirs(
|
|
87
|
-
dirname(targetUserDataDir),
|
|
88
|
-
basename(targetUserDataDir)
|
|
89
|
-
);
|
|
90
|
-
if (!existsSync(sourceProfileDir)) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Chrome profile "${profileDirectory}" was not found in "${resolvedSourceUserDataDir}".`
|
|
93
|
-
);
|
|
63
|
+
var PROCESS_LIST_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
64
|
+
var PS_COMMAND_ENV = { ...process.env, LC_ALL: "C" };
|
|
65
|
+
var LINUX_STAT_START_TIME_FIELD_INDEX = 19;
|
|
66
|
+
var CURRENT_PROCESS_OWNER = {
|
|
67
|
+
pid: process.pid,
|
|
68
|
+
processStartedAtMs: PROCESS_STARTED_AT_MS
|
|
69
|
+
};
|
|
70
|
+
var linuxClockTicksPerSecondPromise = null;
|
|
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;
|
|
94
83
|
}
|
|
95
|
-
const created = await createPersistentProfileClone(
|
|
96
|
-
resolvedSourceUserDataDir,
|
|
97
|
-
sourceProfileDir,
|
|
98
|
-
targetUserDataDir,
|
|
99
|
-
profileDirectory,
|
|
100
|
-
metadata
|
|
101
|
-
);
|
|
102
|
-
await ensurePersistentProfileMetadata(targetUserDataDir, metadata);
|
|
103
84
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
85
|
+
pid,
|
|
86
|
+
processStartedAtMs
|
|
106
87
|
};
|
|
107
88
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
recursive: true
|
|
114
|
-
}).catch(() => void 0)
|
|
115
|
-
)
|
|
116
|
-
);
|
|
89
|
+
function processOwnersEqual(left, right) {
|
|
90
|
+
if (!left || !right) {
|
|
91
|
+
return left === right;
|
|
92
|
+
}
|
|
93
|
+
return left.pid === right.pid && left.processStartedAtMs === right.processStartedAtMs;
|
|
117
94
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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";
|
|
123
110
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
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
|
+
}
|
|
126
119
|
}
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
|
|
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
|
+
};
|
|
130
129
|
}
|
|
131
|
-
function
|
|
132
|
-
return
|
|
130
|
+
function hasMatchingProcessStartTime(expectedStartedAtMs, actualStartedAtMs) {
|
|
131
|
+
return Math.abs(expectedStartedAtMs - actualStartedAtMs) <= PROCESS_START_TIME_TOLERANCE_MS;
|
|
133
132
|
}
|
|
134
|
-
async function
|
|
135
|
-
|
|
133
|
+
async function readProcessStartedAtMs(pid) {
|
|
134
|
+
if (pid <= 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (process.platform === "linux") {
|
|
138
|
+
return await readLinuxProcessStartedAtMs(pid);
|
|
139
|
+
}
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
return await readWindowsProcessStartedAtMs(pid);
|
|
142
|
+
}
|
|
143
|
+
return await readPsProcessStartedAtMs(pid);
|
|
144
|
+
}
|
|
145
|
+
async function readLinuxProcessStartedAtMs(pid) {
|
|
146
|
+
let statRaw;
|
|
136
147
|
try {
|
|
137
|
-
|
|
148
|
+
statRaw = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
138
149
|
} catch {
|
|
139
|
-
return;
|
|
150
|
+
return null;
|
|
140
151
|
}
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (entry === targetProfileDirectory) continue;
|
|
145
|
-
const sourcePath = join(sourceUserDataDir, entry);
|
|
146
|
-
const targetPath = join(targetUserDataDir, entry);
|
|
147
|
-
if (existsSync(targetPath)) continue;
|
|
148
|
-
let entryStat;
|
|
149
|
-
try {
|
|
150
|
-
entryStat = await stat(sourcePath);
|
|
151
|
-
} catch {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (entryStat.isFile()) {
|
|
155
|
-
copyTasks.push(copyFile(sourcePath, targetPath).catch(() => void 0));
|
|
156
|
-
} else if (entryStat.isDirectory()) {
|
|
157
|
-
if (isProfileDirectory(sourceUserDataDir, entry)) continue;
|
|
158
|
-
if (SKIPPED_ROOT_DIRECTORIES.has(entry)) continue;
|
|
159
|
-
copyTasks.push(
|
|
160
|
-
cp(sourcePath, targetPath, { recursive: true }).catch(
|
|
161
|
-
() => void 0
|
|
162
|
-
)
|
|
163
|
-
);
|
|
164
|
-
}
|
|
152
|
+
const startTicks = parseLinuxProcessStartTicks(statRaw);
|
|
153
|
+
if (startTicks === null) {
|
|
154
|
+
return null;
|
|
165
155
|
}
|
|
166
|
-
await Promise.all(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
165
|
);
|
|
173
166
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
167
|
+
function parseLinuxProcessStartTicks(statRaw) {
|
|
168
|
+
const closingParenIndex = statRaw.lastIndexOf(")");
|
|
169
|
+
if (closingParenIndex === -1) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
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;
|
|
180
175
|
}
|
|
181
|
-
async function
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
async function readLinuxBootTimeMs() {
|
|
177
|
+
try {
|
|
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
|
|
186
|
+
);
|
|
187
|
+
return Number.isFinite(bootTimeSeconds) ? bootTimeSeconds * 1e3 : null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
184
190
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
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) {
|
|
189
205
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
216
|
+
}
|
|
197
217
|
);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
const isoTimestamp = stdout.trim();
|
|
219
|
+
if (!isoTimestamp) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
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
|
|
204
237
|
}
|
|
205
|
-
|
|
238
|
+
);
|
|
239
|
+
const startedAt = stdout.trim();
|
|
240
|
+
if (!startedAt) {
|
|
241
|
+
return null;
|
|
206
242
|
}
|
|
207
|
-
|
|
208
|
-
return
|
|
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();
|
|
209
258
|
} finally {
|
|
210
|
-
|
|
211
|
-
|
|
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, {
|
|
212
288
|
recursive: true,
|
|
213
289
|
force: true
|
|
214
290
|
}).catch(() => void 0);
|
|
215
291
|
}
|
|
292
|
+
const owner = await readLockOwner(lockDirPath);
|
|
293
|
+
if ((!owner || await getProcessLiveness(owner) === "dead") && await tryReclaimStaleLock(lockDirPath, owner)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
216
297
|
}
|
|
298
|
+
return async () => {
|
|
299
|
+
await rm(lockDirPath, {
|
|
300
|
+
recursive: true,
|
|
301
|
+
force: true
|
|
302
|
+
}).catch(() => void 0);
|
|
303
|
+
};
|
|
217
304
|
}
|
|
218
|
-
async function
|
|
219
|
-
if (existsSync(
|
|
220
|
-
return;
|
|
305
|
+
async function isDirLockHeld(lockDirPath) {
|
|
306
|
+
if (!existsSync(lockDirPath)) {
|
|
307
|
+
return false;
|
|
221
308
|
}
|
|
222
|
-
await
|
|
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);
|
|
223
314
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
return existsSync(targetUserDataDir) && (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM");
|
|
315
|
+
function getErrorCode(error) {
|
|
316
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
227
317
|
}
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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)
|
|
232
349
|
);
|
|
233
350
|
}
|
|
234
|
-
async function
|
|
235
|
-
|
|
351
|
+
async function tryReclaimStaleLock(lockDirPath, expectedOwner) {
|
|
352
|
+
if (!await tryAcquireLockReclaimer(lockDirPath)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
let reclaimed = false;
|
|
236
356
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
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;
|
|
370
|
+
} finally {
|
|
371
|
+
if (!reclaimed) {
|
|
372
|
+
await rm(buildLockReclaimerDirPath(lockDirPath), {
|
|
373
|
+
recursive: true,
|
|
374
|
+
force: true
|
|
375
|
+
}).catch(() => void 0);
|
|
376
|
+
}
|
|
243
377
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
378
|
+
}
|
|
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
|
+
}
|
|
249
396
|
}
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
420
|
+
}
|
|
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
|
|
435
|
+
);
|
|
436
|
+
}
|
|
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)
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
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;
|
|
1741
|
+
}
|
|
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);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async function restoreClaimedRuntimeProfile(claimedRuntimeUserDataDir, runtimeUserDataDir) {
|
|
1753
|
+
if (!existsSync3(claimedRuntimeUserDataDir)) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (existsSync3(runtimeUserDataDir)) {
|
|
1757
|
+
throw new Error(
|
|
1758
|
+
`Runtime profile "${runtimeUserDataDir}" was recreated before the failed persist could restore it from "${claimedRuntimeUserDataDir}".`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
await rename3(claimedRuntimeUserDataDir, runtimeUserDataDir);
|
|
1762
|
+
}
|
|
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);
|
|
1772
|
+
}
|
|
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);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const runtimeMarker = await readRuntimeProfileCreationMarker(
|
|
1794
|
+
marker.runtimeUserDataDir
|
|
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;
|
|
1815
|
+
}
|
|
1816
|
+
return hasLiveCreation;
|
|
1817
|
+
}
|
|
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);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function parseRuntimeProfileCreationMarker(value) {
|
|
1827
|
+
if (!value || typeof value !== "object") {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
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;
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
creator,
|
|
1840
|
+
persistentUserDataDir,
|
|
1841
|
+
profileDirectory,
|
|
1842
|
+
runtimeUserDataDir
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
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;
|
|
1851
|
+
}
|
|
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;
|
|
1859
|
+
}
|
|
1860
|
+
if (!Number.isInteger(processStartedAtMs) || processStartedAtMs <= 0) {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
return { pid, processStartedAtMs };
|
|
1864
|
+
}
|
|
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="],
|
|
1876
|
+
{
|
|
1877
|
+
env: PS_COMMAND_ENV2,
|
|
1878
|
+
maxBuffer: PROCESS_LIST_MAX_BUFFER_BYTES2
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1882
|
+
} catch {
|
|
1883
|
+
return [];
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
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
|
+
}
|
|
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 [];
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
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;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return [
|
|
1916
|
+
`--user-data-dir="${userDataDir}"`,
|
|
1917
|
+
`--user-data-dir='${userDataDir}'`
|
|
1918
|
+
].some((candidate) => commandLine.includes(candidate));
|
|
1919
|
+
}
|
|
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
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
detached: true,
|
|
1977
|
+
killStrategy: "process-group",
|
|
1978
|
+
shouldUnref: true
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
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);
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
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;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
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
|
|
2081
|
+
});
|
|
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
|
|
2109
|
+
});
|
|
2110
|
+
throw error;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
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
|
+
);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
await context.page.close().catch(() => void 0);
|
|
2128
|
+
await context.browser.close().catch(() => void 0);
|
|
2129
|
+
}
|
|
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
|
+
);
|
|
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") {
|
|
2156
|
+
return {
|
|
2157
|
+
kind: "ready",
|
|
2158
|
+
metadata
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
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
|
+
};
|
|
2172
|
+
}
|
|
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();
|
|
2192
|
+
}
|
|
2193
|
+
try {
|
|
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
|
+
};
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
await killSpawnedBrowserProcess(processHandle);
|
|
2217
|
+
await rm4(buildSharedSessionDirPath(persistentUserDataDir), {
|
|
2218
|
+
force: true,
|
|
2219
|
+
recursive: true
|
|
2220
|
+
}).catch(() => void 0);
|
|
2221
|
+
throw error;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
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;
|
|
2245
|
+
}
|
|
2246
|
+
await rm4(
|
|
2247
|
+
buildSharedSessionDirPath(
|
|
2248
|
+
reservation.metadata.persistentUserDataDir
|
|
2249
|
+
),
|
|
2250
|
+
{
|
|
2251
|
+
force: true,
|
|
2252
|
+
recursive: true
|
|
2253
|
+
}
|
|
2254
|
+
).catch(() => void 0);
|
|
2255
|
+
}
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
);
|
|
2259
|
+
if (shouldPreserveLiveBrowser) {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
await killOwnedBrowserProcess(reservation.launchedBrowserOwner);
|
|
2263
|
+
await waitForProcessToExit(reservation.launchedBrowserOwner, 2e3);
|
|
2264
|
+
}
|
|
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);
|
|
2275
|
+
}
|
|
2276
|
+
async function waitForSharedSessionReady(metadata, timeoutMs) {
|
|
2277
|
+
await resolveCdpWebSocketUrl(
|
|
2278
|
+
buildSharedSessionDiscoveryUrl(metadata.debugPort),
|
|
2279
|
+
timeoutMs
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
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;
|
|
2293
|
+
}
|
|
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
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
async function killOwnedBrowserProcess(owner) {
|
|
2305
|
+
if (await getProcessLiveness(owner) === "dead") {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
await killOwnedBrowserProcessByPid(owner.pid);
|
|
2309
|
+
}
|
|
2310
|
+
async function killSpawnedBrowserProcess(processHandle) {
|
|
2311
|
+
const pid = processHandle.pid;
|
|
2312
|
+
if (!pid || processHandle.exitCode !== null) {
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
await killOwnedBrowserProcessByPid(pid);
|
|
2316
|
+
await waitForPidToExit(pid, 2e3);
|
|
2317
|
+
}
|
|
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;
|
|
2333
|
+
}
|
|
2334
|
+
if (processPolicy.killStrategy === "process-group") {
|
|
2335
|
+
try {
|
|
2336
|
+
process.kill(-pid, "SIGKILL");
|
|
2337
|
+
return;
|
|
2338
|
+
} catch {
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
process.kill(pid, "SIGKILL");
|
|
2343
|
+
} catch {
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
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;
|
|
2351
|
+
}
|
|
2352
|
+
await sleep5(50);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
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);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
async function waitForSpawnedProcessOwner(pid, timeoutMs) {
|
|
2365
|
+
if (!pid || pid <= 0) {
|
|
2366
|
+
throw new Error("Chrome did not expose a child process id.");
|
|
2367
|
+
}
|
|
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);
|
|
2375
|
+
}
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`Chrome process ${pid} did not report a stable process start time.`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
async function withSharedSessionLock(persistentUserDataDir, action) {
|
|
2381
|
+
return await withDirLock(
|
|
2382
|
+
buildSharedSessionLockPath(persistentUserDataDir),
|
|
2383
|
+
action
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
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
|
+
}
|
|
2398
|
+
);
|
|
2399
|
+
return {
|
|
2400
|
+
client,
|
|
2401
|
+
metadata,
|
|
2402
|
+
reuseExistingPage: liveClients.length === 0
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
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);
|
|
2412
|
+
let entries;
|
|
2413
|
+
try {
|
|
2414
|
+
entries = await readdir3(clientsDirPath, {
|
|
2415
|
+
encoding: "utf8",
|
|
2416
|
+
withFileTypes: true
|
|
2417
|
+
});
|
|
2418
|
+
} catch {
|
|
2419
|
+
return [];
|
|
2420
|
+
}
|
|
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;
|
|
2439
|
+
}
|
|
2440
|
+
async function readSharedSessionClientRegistration(filePath) {
|
|
2441
|
+
try {
|
|
2442
|
+
const raw = await readFile5(filePath, "utf8");
|
|
2443
|
+
return parseSharedSessionClientRegistration(JSON.parse(raw));
|
|
2444
|
+
} catch {
|
|
2445
|
+
return null;
|
|
2446
|
+
}
|
|
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") {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
const parsed = value;
|
|
2460
|
+
const owner = parseProcessOwner(parsed.owner);
|
|
2461
|
+
if (!owner || typeof parsed.clientId !== "string" || typeof parsed.createdAt !== "string") {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
return {
|
|
2465
|
+
clientId: parsed.clientId,
|
|
2466
|
+
createdAt: parsed.createdAt,
|
|
2467
|
+
owner
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
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
|
+
);
|
|
2475
|
+
}
|
|
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
|
+
);
|
|
2480
|
+
}
|
|
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);
|
|
2524
|
+
}
|
|
2525
|
+
if (await getProcessLiveness(closePlan.browserOwner) !== "dead") {
|
|
2526
|
+
await killOwnedBrowserProcess(closePlan.browserOwner);
|
|
2527
|
+
await waitForProcessToExit(closePlan.browserOwner, 2e3);
|
|
2528
|
+
}
|
|
2529
|
+
await finalizeSharedSessionClose(persistentUserDataDir, closePlan.sessionId);
|
|
2530
|
+
}
|
|
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
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
return contexts[0];
|
|
2576
|
+
}
|
|
2577
|
+
async function getSharedSessionPage(context, reuseExistingPage) {
|
|
2578
|
+
if (reuseExistingPage) {
|
|
2579
|
+
return await getExistingPageOrCreate(context);
|
|
2580
|
+
}
|
|
2581
|
+
return await context.newPage();
|
|
2582
|
+
}
|
|
2583
|
+
async function getExistingPageOrCreate(context) {
|
|
2584
|
+
const existingPage = context.pages()[0];
|
|
2585
|
+
if (existingPage) {
|
|
2586
|
+
return existingPage;
|
|
2587
|
+
}
|
|
2588
|
+
return await context.newPage();
|
|
2589
|
+
}
|
|
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.";
|
|
252
2615
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
})
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
function isTempDirOwnedByLiveProcess(tempDirName, tempDirPrefix) {
|
|
261
|
-
const owner = parseTempDirOwner(tempDirName, tempDirPrefix);
|
|
262
|
-
if (!owner) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
if (owner.pid === process.pid && Math.abs(owner.processStartedAtMs - PROCESS_STARTED_AT_MS) <= PROCESS_START_TIME_TOLERANCE_MS) {
|
|
266
|
-
return true;
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
2618
|
+
}
|
|
2619
|
+
await sleep5(100);
|
|
267
2620
|
}
|
|
268
|
-
|
|
2621
|
+
throw new Error(
|
|
2622
|
+
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
2623
|
+
);
|
|
269
2624
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
2625
|
+
function normalizeDiscoveryUrl(cdpUrl) {
|
|
2626
|
+
let parsed;
|
|
2627
|
+
try {
|
|
2628
|
+
parsed = new URL(cdpUrl);
|
|
2629
|
+
} catch {
|
|
2630
|
+
throw new Error(
|
|
2631
|
+
`Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
|
|
2632
|
+
);
|
|
276
2633
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
remainder.slice(firstDashIndex + 1, secondDashIndex),
|
|
280
|
-
10
|
|
281
|
-
);
|
|
282
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
283
|
-
return null;
|
|
2634
|
+
if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
|
|
2635
|
+
return parsed;
|
|
284
2636
|
}
|
|
285
|
-
if (
|
|
286
|
-
|
|
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).`
|
|
2640
|
+
);
|
|
287
2641
|
}
|
|
288
|
-
|
|
2642
|
+
const normalized = new URL(parsed.toString());
|
|
2643
|
+
normalized.pathname = "/json/version";
|
|
2644
|
+
normalized.search = "";
|
|
2645
|
+
normalized.hash = "";
|
|
2646
|
+
return normalized;
|
|
289
2647
|
}
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
async function sleep5(ms) {
|
|
2671
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
298
2672
|
}
|
|
299
2673
|
|
|
300
2674
|
// src/browser/pool.ts
|
|
301
|
-
import { spawn } from "child_process";
|
|
302
|
-
import { rm as rm2 } from "fs/promises";
|
|
303
|
-
import { createServer } from "net";
|
|
304
2675
|
import {
|
|
305
|
-
chromium
|
|
2676
|
+
chromium as chromium2
|
|
306
2677
|
} from "playwright";
|
|
307
2678
|
|
|
308
2679
|
// src/browser/cdp-proxy.ts
|
|
@@ -722,16 +3093,14 @@ function errorMessage(error) {
|
|
|
722
3093
|
// src/browser/pool.ts
|
|
723
3094
|
var BrowserPool = class {
|
|
724
3095
|
browser = null;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
managedUserDataDir = null;
|
|
728
|
-
persistentProfile = false;
|
|
3096
|
+
activeSessionClose = null;
|
|
3097
|
+
closeInFlight = null;
|
|
729
3098
|
defaults;
|
|
730
3099
|
constructor(defaults = {}) {
|
|
731
3100
|
this.defaults = defaults;
|
|
732
3101
|
}
|
|
733
3102
|
async launch(options = {}) {
|
|
734
|
-
if (this.browser || this.
|
|
3103
|
+
if (this.browser || this.activeSessionClose) {
|
|
735
3104
|
await this.close();
|
|
736
3105
|
}
|
|
737
3106
|
const mode = options.mode ?? this.defaults.mode ?? "chromium";
|
|
@@ -778,30 +3147,26 @@ var BrowserPool = class {
|
|
|
778
3147
|
return this.launchSandbox(options);
|
|
779
3148
|
}
|
|
780
3149
|
async close() {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const
|
|
786
|
-
this.
|
|
787
|
-
this.cdpProxy = null;
|
|
788
|
-
this.launchedProcess = null;
|
|
789
|
-
this.managedUserDataDir = null;
|
|
790
|
-
this.persistentProfile = false;
|
|
3150
|
+
if (this.closeInFlight) {
|
|
3151
|
+
await this.closeInFlight;
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
const closeOperation = this.closeCurrent();
|
|
3155
|
+
this.closeInFlight = closeOperation;
|
|
791
3156
|
try {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
3157
|
+
await closeOperation;
|
|
3158
|
+
this.browser = null;
|
|
3159
|
+
this.activeSessionClose = null;
|
|
795
3160
|
} finally {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
3161
|
+
this.closeInFlight = null;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
async closeCurrent() {
|
|
3165
|
+
if (this.activeSessionClose) {
|
|
3166
|
+
await this.activeSessionClose();
|
|
3167
|
+
return;
|
|
804
3168
|
}
|
|
3169
|
+
await this.browser?.close().catch(() => void 0);
|
|
805
3170
|
}
|
|
806
3171
|
async connectToRunning(cdpUrl, timeout) {
|
|
807
3172
|
let browser = null;
|
|
@@ -816,11 +3181,14 @@ var BrowserPool = class {
|
|
|
816
3181
|
}
|
|
817
3182
|
cdpProxy = new CDPProxy(browserWsUrl, targetId);
|
|
818
3183
|
const proxyWsUrl = await cdpProxy.start();
|
|
819
|
-
browser = await
|
|
3184
|
+
browser = await chromium2.connectOverCDP(proxyWsUrl, {
|
|
820
3185
|
timeout: timeout ?? 3e4
|
|
821
3186
|
});
|
|
822
3187
|
this.browser = browser;
|
|
823
|
-
this.
|
|
3188
|
+
this.activeSessionClose = async () => {
|
|
3189
|
+
await browser?.close().catch(() => void 0);
|
|
3190
|
+
cdpProxy?.close();
|
|
3191
|
+
};
|
|
824
3192
|
const { context, page } = await pickBrowserContextAndPage(browser);
|
|
825
3193
|
return { browser, context, page, isExternal: true };
|
|
826
3194
|
} catch (error) {
|
|
@@ -829,7 +3197,7 @@ var BrowserPool = class {
|
|
|
829
3197
|
}
|
|
830
3198
|
cdpProxy?.close();
|
|
831
3199
|
this.browser = null;
|
|
832
|
-
this.
|
|
3200
|
+
this.activeSessionClose = null;
|
|
833
3201
|
throw error;
|
|
834
3202
|
}
|
|
835
3203
|
}
|
|
@@ -849,55 +3217,29 @@ var BrowserPool = class {
|
|
|
849
3217
|
sourceUserDataDir,
|
|
850
3218
|
profileDirectory
|
|
851
3219
|
);
|
|
852
|
-
await
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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,
|
|
861
3229
|
profileDirectory,
|
|
862
|
-
|
|
863
|
-
headless
|
|
864
|
-
});
|
|
865
|
-
const processHandle = spawn(executablePath, launchArgs, {
|
|
866
|
-
detached: process.platform !== "win32",
|
|
867
|
-
stdio: "ignore"
|
|
3230
|
+
timeoutMs: options.timeout ?? 3e4
|
|
868
3231
|
});
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
timeout: options.timeout ?? 3e4
|
|
878
|
-
});
|
|
879
|
-
const { context, page } = await createOwnedBrowserContextAndPage(
|
|
880
|
-
browser
|
|
881
|
-
);
|
|
882
|
-
if (options.initialUrl) {
|
|
883
|
-
await page.goto(options.initialUrl, {
|
|
884
|
-
waitUntil: "domcontentloaded",
|
|
885
|
-
timeout: options.timeout ?? 3e4
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
this.browser = browser;
|
|
889
|
-
this.launchedProcess = processHandle;
|
|
890
|
-
this.managedUserDataDir = persistentProfile.userDataDir;
|
|
891
|
-
this.persistentProfile = true;
|
|
892
|
-
return { browser, context, page, isExternal: false };
|
|
893
|
-
} catch (error) {
|
|
894
|
-
await browser?.close().catch(() => void 0);
|
|
895
|
-
await killProcessTree(processHandle);
|
|
896
|
-
throw error;
|
|
897
|
-
}
|
|
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
|
+
};
|
|
898
3240
|
}
|
|
899
3241
|
async launchSandbox(options) {
|
|
900
|
-
const browser = await
|
|
3242
|
+
const browser = await chromium2.launch({
|
|
901
3243
|
headless: resolveLaunchHeadless(
|
|
902
3244
|
"chromium",
|
|
903
3245
|
options.headless,
|
|
@@ -910,11 +3252,14 @@ var BrowserPool = class {
|
|
|
910
3252
|
const context = await browser.newContext(options.context || {});
|
|
911
3253
|
const page = await context.newPage();
|
|
912
3254
|
this.browser = browser;
|
|
3255
|
+
this.activeSessionClose = async () => {
|
|
3256
|
+
await browser.close().catch(() => void 0);
|
|
3257
|
+
};
|
|
913
3258
|
return { browser, context, page, isExternal: false };
|
|
914
3259
|
}
|
|
915
3260
|
};
|
|
916
3261
|
async function pickBrowserContextAndPage(browser) {
|
|
917
|
-
const context =
|
|
3262
|
+
const context = getPrimaryBrowserContext2(browser);
|
|
918
3263
|
const page = await getAttachedPageOrCreate(context);
|
|
919
3264
|
return { context, page };
|
|
920
3265
|
}
|
|
@@ -927,11 +3272,6 @@ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
|
|
|
927
3272
|
}
|
|
928
3273
|
return mode === "real";
|
|
929
3274
|
}
|
|
930
|
-
async function createOwnedBrowserContextAndPage(browser) {
|
|
931
|
-
const context = getPrimaryBrowserContext(browser);
|
|
932
|
-
const page = await getExistingPageOrCreate(context);
|
|
933
|
-
return { context, page };
|
|
934
|
-
}
|
|
935
3275
|
async function getAttachedPageOrCreate(context) {
|
|
936
3276
|
const pages = context.pages();
|
|
937
3277
|
const inspectablePage = pages.find(
|
|
@@ -946,14 +3286,7 @@ async function getAttachedPageOrCreate(context) {
|
|
|
946
3286
|
}
|
|
947
3287
|
return await context.newPage();
|
|
948
3288
|
}
|
|
949
|
-
|
|
950
|
-
const existingPage = context.pages()[0];
|
|
951
|
-
if (existingPage) {
|
|
952
|
-
return existingPage;
|
|
953
|
-
}
|
|
954
|
-
return await context.newPage();
|
|
955
|
-
}
|
|
956
|
-
function getPrimaryBrowserContext(browser) {
|
|
3289
|
+
function getPrimaryBrowserContext2(browser) {
|
|
957
3290
|
const contexts = browser.contexts();
|
|
958
3291
|
if (contexts.length === 0) {
|
|
959
3292
|
throw new Error(
|
|
@@ -972,125 +3305,6 @@ function safePageUrl(page) {
|
|
|
972
3305
|
return "";
|
|
973
3306
|
}
|
|
974
3307
|
}
|
|
975
|
-
function normalizeDiscoveryUrl(cdpUrl) {
|
|
976
|
-
let parsed;
|
|
977
|
-
try {
|
|
978
|
-
parsed = new URL(cdpUrl);
|
|
979
|
-
} catch {
|
|
980
|
-
throw new Error(
|
|
981
|
-
`Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
|
|
985
|
-
return parsed;
|
|
986
|
-
}
|
|
987
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
988
|
-
throw new Error(
|
|
989
|
-
`Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
const normalized = new URL(parsed.toString());
|
|
993
|
-
normalized.pathname = "/json/version";
|
|
994
|
-
normalized.search = "";
|
|
995
|
-
normalized.hash = "";
|
|
996
|
-
return normalized;
|
|
997
|
-
}
|
|
998
|
-
async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
|
|
999
|
-
if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
|
|
1000
|
-
return cdpUrl;
|
|
1001
|
-
}
|
|
1002
|
-
const versionUrl = normalizeDiscoveryUrl(cdpUrl);
|
|
1003
|
-
const deadline = Date.now() + timeoutMs;
|
|
1004
|
-
let lastError = "CDP discovery did not respond.";
|
|
1005
|
-
while (Date.now() < deadline) {
|
|
1006
|
-
const remaining = Math.max(deadline - Date.now(), 1e3);
|
|
1007
|
-
try {
|
|
1008
|
-
const response = await fetch(versionUrl, {
|
|
1009
|
-
signal: AbortSignal.timeout(Math.min(remaining, 5e3))
|
|
1010
|
-
});
|
|
1011
|
-
if (!response.ok) {
|
|
1012
|
-
lastError = `${response.status} ${response.statusText}`;
|
|
1013
|
-
} else {
|
|
1014
|
-
const payload = await response.json();
|
|
1015
|
-
const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
|
|
1016
|
-
if (wsUrl && wsUrl.trim()) {
|
|
1017
|
-
return wsUrl;
|
|
1018
|
-
}
|
|
1019
|
-
lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
|
|
1020
|
-
}
|
|
1021
|
-
} catch (error) {
|
|
1022
|
-
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
1023
|
-
}
|
|
1024
|
-
await sleep(100);
|
|
1025
|
-
}
|
|
1026
|
-
throw new Error(
|
|
1027
|
-
`Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
async function reserveDebugPort() {
|
|
1031
|
-
return await new Promise((resolve, reject) => {
|
|
1032
|
-
const server = createServer();
|
|
1033
|
-
server.unref();
|
|
1034
|
-
server.on("error", reject);
|
|
1035
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1036
|
-
const address = server.address();
|
|
1037
|
-
if (!address || typeof address === "string") {
|
|
1038
|
-
server.close();
|
|
1039
|
-
reject(new Error("Failed to reserve a local debug port."));
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
server.close((error) => {
|
|
1043
|
-
if (error) {
|
|
1044
|
-
reject(error);
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
resolve(address.port);
|
|
1048
|
-
});
|
|
1049
|
-
});
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
function buildRealBrowserLaunchArgs(options) {
|
|
1053
|
-
const args = [
|
|
1054
|
-
`--user-data-dir=${options.userDataDir}`,
|
|
1055
|
-
`--profile-directory=${options.profileDirectory}`,
|
|
1056
|
-
`--remote-debugging-port=${options.debugPort}`,
|
|
1057
|
-
"--disable-blink-features=AutomationControlled"
|
|
1058
|
-
];
|
|
1059
|
-
if (options.headless) {
|
|
1060
|
-
args.push("--headless=new");
|
|
1061
|
-
}
|
|
1062
|
-
return args;
|
|
1063
|
-
}
|
|
1064
|
-
async function killProcessTree(processHandle) {
|
|
1065
|
-
if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
if (process.platform === "win32") {
|
|
1069
|
-
await new Promise((resolve) => {
|
|
1070
|
-
const killer = spawn(
|
|
1071
|
-
"taskkill",
|
|
1072
|
-
["/pid", String(processHandle.pid), "/t", "/f"],
|
|
1073
|
-
{
|
|
1074
|
-
stdio: "ignore"
|
|
1075
|
-
}
|
|
1076
|
-
);
|
|
1077
|
-
killer.on("error", () => resolve());
|
|
1078
|
-
killer.on("exit", () => resolve());
|
|
1079
|
-
});
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
try {
|
|
1083
|
-
process.kill(-processHandle.pid, "SIGKILL");
|
|
1084
|
-
} catch {
|
|
1085
|
-
try {
|
|
1086
|
-
processHandle.kill("SIGKILL");
|
|
1087
|
-
} catch {
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
async function sleep(ms) {
|
|
1092
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1093
|
-
}
|
|
1094
3308
|
|
|
1095
3309
|
// src/navigation.ts
|
|
1096
3310
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -1470,7 +3684,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
1470
3684
|
TRANSIENT_CONTEXT_RETRY_DELAY_MS,
|
|
1471
3685
|
Math.max(0, deadline - Date.now())
|
|
1472
3686
|
);
|
|
1473
|
-
await
|
|
3687
|
+
await sleep6(retryDelay);
|
|
1474
3688
|
}
|
|
1475
3689
|
}
|
|
1476
3690
|
}
|
|
@@ -1503,7 +3717,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
|
|
|
1503
3717
|
() => ({ kind: "resolved" }),
|
|
1504
3718
|
(error) => ({ kind: "rejected", error })
|
|
1505
3719
|
);
|
|
1506
|
-
const timeoutPromise =
|
|
3720
|
+
const timeoutPromise = sleep6(
|
|
1507
3721
|
timeout + FRAME_EVALUATE_GRACE_MS
|
|
1508
3722
|
).then(() => ({ kind: "timeout" }));
|
|
1509
3723
|
const result = await Promise.race([
|
|
@@ -1645,7 +3859,7 @@ function isIgnorableFrameError(error) {
|
|
|
1645
3859
|
const message = error.message;
|
|
1646
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");
|
|
1647
3861
|
}
|
|
1648
|
-
function
|
|
3862
|
+
function sleep6(ms) {
|
|
1649
3863
|
return new Promise((resolve) => {
|
|
1650
3864
|
setTimeout(resolve, ms);
|
|
1651
3865
|
});
|
|
@@ -5897,7 +8111,7 @@ async function closeTab(context, activePage, index) {
|
|
|
5897
8111
|
}
|
|
5898
8112
|
|
|
5899
8113
|
// src/actions/cookies.ts
|
|
5900
|
-
import { readFile, writeFile as
|
|
8114
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
5901
8115
|
async function getCookies(context, url) {
|
|
5902
8116
|
return context.cookies(url ? [url] : void 0);
|
|
5903
8117
|
}
|
|
@@ -5909,10 +8123,10 @@ async function clearCookies(context) {
|
|
|
5909
8123
|
}
|
|
5910
8124
|
async function exportCookies(context, filePath, url) {
|
|
5911
8125
|
const cookies = await context.cookies(url ? [url] : void 0);
|
|
5912
|
-
await
|
|
8126
|
+
await writeFile5(filePath, JSON.stringify(cookies, null, 2), "utf-8");
|
|
5913
8127
|
}
|
|
5914
8128
|
async function importCookies(context, filePath) {
|
|
5915
|
-
const raw = await
|
|
8129
|
+
const raw = await readFile6(filePath, "utf-8");
|
|
5916
8130
|
const cookies = JSON.parse(raw);
|
|
5917
8131
|
await context.addCookies(cookies);
|
|
5918
8132
|
}
|
|
@@ -7740,7 +9954,7 @@ async function executeAgentAction(page, action) {
|
|
|
7740
9954
|
}
|
|
7741
9955
|
case "wait": {
|
|
7742
9956
|
const ms = numberOr(action.timeMs, action.time_ms, 1e3);
|
|
7743
|
-
await
|
|
9957
|
+
await sleep7(ms);
|
|
7744
9958
|
return;
|
|
7745
9959
|
}
|
|
7746
9960
|
case "goto": {
|
|
@@ -7905,7 +10119,7 @@ async function pressKeyCombo(page, combo) {
|
|
|
7905
10119
|
}
|
|
7906
10120
|
}
|
|
7907
10121
|
}
|
|
7908
|
-
function
|
|
10122
|
+
function sleep7(ms) {
|
|
7909
10123
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7910
10124
|
}
|
|
7911
10125
|
|
|
@@ -7936,7 +10150,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
7936
10150
|
if (isMutatingAgentAction(action)) {
|
|
7937
10151
|
this.onMutatingAction?.(action);
|
|
7938
10152
|
}
|
|
7939
|
-
await
|
|
10153
|
+
await sleep8(this.config.waitBetweenActionsMs);
|
|
7940
10154
|
});
|
|
7941
10155
|
try {
|
|
7942
10156
|
const result = await this.client.execute({
|
|
@@ -7998,7 +10212,7 @@ var OpensteerCuaAgentHandler = class {
|
|
|
7998
10212
|
await this.cursorController.preview({ x, y }, "agent");
|
|
7999
10213
|
}
|
|
8000
10214
|
};
|
|
8001
|
-
function
|
|
10215
|
+
function sleep8(ms) {
|
|
8002
10216
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
8003
10217
|
}
|
|
8004
10218
|
|
|
@@ -8434,7 +10648,7 @@ var CursorController = class {
|
|
|
8434
10648
|
for (const step of motion.points) {
|
|
8435
10649
|
await this.renderer.move(step, this.style);
|
|
8436
10650
|
if (motion.stepDelayMs > 0) {
|
|
8437
|
-
await
|
|
10651
|
+
await sleep9(motion.stepDelayMs);
|
|
8438
10652
|
}
|
|
8439
10653
|
}
|
|
8440
10654
|
if (shouldPulse(intent)) {
|
|
@@ -8592,12 +10806,12 @@ function clamp2(value, min, max) {
|
|
|
8592
10806
|
function shouldPulse(intent) {
|
|
8593
10807
|
return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
|
|
8594
10808
|
}
|
|
8595
|
-
function
|
|
10809
|
+
function sleep9(ms) {
|
|
8596
10810
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8597
10811
|
}
|
|
8598
10812
|
|
|
8599
10813
|
// src/opensteer.ts
|
|
8600
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
10814
|
+
import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
|
|
8601
10815
|
|
|
8602
10816
|
// src/action-wait.ts
|
|
8603
10817
|
var ROBUST_PROFILE = {
|
|
@@ -8792,7 +11006,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8792
11006
|
this.idleSince = 0;
|
|
8793
11007
|
}
|
|
8794
11008
|
const remaining = Math.max(1, options.deadline - now);
|
|
8795
|
-
await
|
|
11009
|
+
await sleep10(Math.min(NETWORK_POLL_MS, remaining));
|
|
8796
11010
|
}
|
|
8797
11011
|
}
|
|
8798
11012
|
handleRequestStarted = (request) => {
|
|
@@ -8837,7 +11051,7 @@ var AdaptiveNetworkTracker = class {
|
|
|
8837
11051
|
return false;
|
|
8838
11052
|
}
|
|
8839
11053
|
};
|
|
8840
|
-
async function
|
|
11054
|
+
async function sleep10(ms) {
|
|
8841
11055
|
await new Promise((resolve) => {
|
|
8842
11056
|
setTimeout(resolve, ms);
|
|
8843
11057
|
});
|
|
@@ -10617,15 +12831,22 @@ var Opensteer = class _Opensteer {
|
|
|
10617
12831
|
}
|
|
10618
12832
|
return;
|
|
10619
12833
|
}
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
12834
|
+
let closedOwnedBrowser = false;
|
|
12835
|
+
try {
|
|
12836
|
+
if (this.ownsBrowser) {
|
|
12837
|
+
await this.pool.close();
|
|
12838
|
+
closedOwnedBrowser = true;
|
|
12839
|
+
}
|
|
12840
|
+
} finally {
|
|
12841
|
+
this.browser = null;
|
|
12842
|
+
this.pageRef = null;
|
|
12843
|
+
this.contextRef = null;
|
|
12844
|
+
if (!this.ownsBrowser || closedOwnedBrowser) {
|
|
12845
|
+
this.ownsBrowser = false;
|
|
12846
|
+
}
|
|
12847
|
+
if (this.cursorController) {
|
|
12848
|
+
await this.cursorController.dispose().catch(() => void 0);
|
|
12849
|
+
}
|
|
10629
12850
|
}
|
|
10630
12851
|
}
|
|
10631
12852
|
async syncLocalSelectorCacheToCloud() {
|
|
@@ -12949,12 +15170,16 @@ function normalizeCloudBrowserProfilePreference(value, source) {
|
|
|
12949
15170
|
}
|
|
12950
15171
|
function buildLocalRunId(namespace) {
|
|
12951
15172
|
const normalized = namespace.trim() || "default";
|
|
12952
|
-
return `${normalized}-${Date.now().toString(36)}-${
|
|
15173
|
+
return `${normalized}-${Date.now().toString(36)}-${randomUUID5().slice(0, 8)}`;
|
|
12953
15174
|
}
|
|
12954
15175
|
|
|
12955
15176
|
export {
|
|
12956
15177
|
getOrCreatePersistentProfile,
|
|
12957
15178
|
clearPersistentProfileSingletons,
|
|
15179
|
+
createIsolatedRuntimeProfile,
|
|
15180
|
+
persistIsolatedRuntimeProfile,
|
|
15181
|
+
hasActiveRuntimeProfileCreations,
|
|
15182
|
+
getOwnedRealBrowserProcessPolicy,
|
|
12958
15183
|
BrowserPool,
|
|
12959
15184
|
waitForVisualStability,
|
|
12960
15185
|
createEmptyRegistry,
|