@trusty-squire/mcp 1.1.13-rc.8 → 1.1.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/README.md +139 -62
- package/assets/login/vnc-input.js +9 -0
- package/assets/login/vnc.html +214 -202
- package/assets/screencaps/connect-walkthrough.png +0 -0
- package/assets/screencaps/connect-walkthrough.svg +1 -1
- package/dist/api-client.d.ts +26 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +19 -31
- package/dist/api-client.js.map +1 -1
- package/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/bot/browser.d.ts +134 -77
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1788 -1543
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts +167 -0
- package/dist/bot/compact-observation-v2.d.ts.map +1 -0
- package/dist/bot/compact-observation-v2.js +1038 -0
- package/dist/bot/compact-observation-v2.js.map +1 -0
- package/dist/bot/credential-shape.d.ts +2 -0
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +21 -8
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/element-fingerprint.d.ts +9 -0
- package/dist/bot/element-fingerprint.d.ts.map +1 -0
- package/dist/bot/element-fingerprint.js +147 -0
- package/dist/bot/element-fingerprint.js.map +1 -0
- package/dist/bot/email-verification.d.ts +7 -1
- package/dist/bot/email-verification.d.ts.map +1 -1
- package/dist/bot/email-verification.js +48 -14
- package/dist/bot/email-verification.js.map +1 -1
- package/dist/bot/google-login.d.ts +28 -52
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +246 -1119
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/install-completion.d.ts +1 -2
- package/dist/bot/install-completion.d.ts.map +1 -1
- package/dist/bot/install-completion.js +1 -2
- package/dist/bot/install-completion.js.map +1 -1
- package/dist/bot/login-state.d.ts +13 -7
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +50 -114
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/oauth-providers.d.ts +1 -1
- package/dist/bot/oauth-providers.d.ts.map +1 -1
- package/dist/bot/oauth-providers.js +3 -3
- package/dist/bot/oauth-providers.js.map +1 -1
- package/dist/bot/oauth-scope.d.ts +4 -0
- package/dist/bot/oauth-scope.d.ts.map +1 -0
- package/dist/bot/oauth-scope.js +58 -0
- package/dist/bot/oauth-scope.js.map +1 -0
- package/dist/bot/operator-browser-watchdog.d.ts +127 -0
- package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
- package/dist/bot/operator-browser-watchdog.js +438 -0
- package/dist/bot/operator-browser-watchdog.js.map +1 -0
- package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
- package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper-worker.js +17 -0
- package/dist/bot/owner-process-reaper-worker.js.map +1 -0
- package/dist/bot/owner-process-reaper.d.ts +103 -0
- package/dist/bot/owner-process-reaper.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper.js +1058 -0
- package/dist/bot/owner-process-reaper.js.map +1 -0
- package/dist/bot/pay-operator.d.ts +13 -2
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +319 -123
- package/dist/bot/pay-operator.js.map +1 -1
- package/dist/bot/profile.d.ts +10 -1
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +137 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/promote-to-skill.js +1 -1
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/provision-session.d.ts +60 -164
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +2049 -1546
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/remote-login-display.d.ts +61 -0
- package/dist/bot/remote-login-display.d.ts.map +1 -0
- package/dist/bot/remote-login-display.js +765 -0
- package/dist/bot/remote-login-display.js.map +1 -0
- package/dist/bot/session/hosts.d.ts +8 -0
- package/dist/bot/session/hosts.d.ts.map +1 -0
- package/dist/bot/session/hosts.js +36 -0
- package/dist/bot/session/hosts.js.map +1 -0
- package/dist/bot/session/lifecycle.d.ts +56 -0
- package/dist/bot/session/lifecycle.d.ts.map +1 -0
- package/dist/bot/session/lifecycle.js +962 -0
- package/dist/bot/session/lifecycle.js.map +1 -0
- package/dist/bot/session/model.d.ts +175 -0
- package/dist/bot/session/model.d.ts.map +1 -0
- package/dist/bot/session/model.js +77 -0
- package/dist/bot/session/model.js.map +1 -0
- package/dist/install/cli.d.ts +29 -9
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +243 -322
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +1 -0
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +19 -5
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/ui.d.ts.map +1 -1
- package/dist/install/ui.js +3 -15
- package/dist/install/ui.js.map +1 -1
- package/dist/operator-env.js +1 -1
- package/dist/operator-env.js.map +1 -1
- package/dist/server-instance-registry.d.ts +103 -0
- package/dist/server-instance-registry.d.ts.map +1 -0
- package/dist/server-instance-registry.js +392 -0
- package/dist/server-instance-registry.js.map +1 -0
- package/dist/server.d.ts +15 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -62
- package/dist/server.js.map +1 -1
- package/dist/session-guard.d.ts +28 -0
- package/dist/session-guard.d.ts.map +1 -0
- package/dist/session-guard.js +84 -0
- package/dist/session-guard.js.map +1 -0
- package/dist/session.d.ts +57 -13
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +246 -127
- package/dist/session.js.map +1 -1
- package/dist/tools/audit-log.d.ts +16 -2
- package/dist/tools/audit-log.d.ts.map +1 -1
- package/dist/tools/audit-log.js +223 -13
- package/dist/tools/audit-log.js.map +1 -1
- package/dist/tools/audit-rollup.d.ts +80 -0
- package/dist/tools/audit-rollup.d.ts.map +1 -0
- package/dist/tools/audit-rollup.js +0 -0
- package/dist/tools/audit-rollup.js.map +1 -0
- package/dist/tools/fetch-credential.d.ts +37 -0
- package/dist/tools/fetch-credential.d.ts.map +1 -0
- package/dist/tools/fetch-credential.js +153 -0
- package/dist/tools/fetch-credential.js.map +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +4 -4
- package/dist/tools/operate-pay.d.ts.map +1 -1
- package/dist/tools/operate-pay.js +163 -96
- package/dist/tools/operate-pay.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +43 -35
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +268 -164
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/use-credential.d.ts.map +1 -1
- package/dist/tools/use-credential.js +3 -2
- package/dist/tools/use-credential.js.map +1 -1
- package/package.json +4 -6
- package/dist/bot/operator-direct-identity.d.ts +0 -26
- package/dist/bot/operator-direct-identity.d.ts.map +0 -1
- package/dist/bot/operator-direct-identity.js +0 -120
- package/dist/bot/operator-direct-identity.js.map +0 -1
- package/dist/bot/operator-profile-pool.d.ts +0 -97
- package/dist/bot/operator-profile-pool.d.ts.map +0 -1
- package/dist/bot/operator-profile-pool.js +0 -855
- package/dist/bot/operator-profile-pool.js.map +0 -1
- package/dist/bot/xvfb.d.ts +0 -13
- package/dist/bot/xvfb.d.ts.map +0 -1
- package/dist/bot/xvfb.js +0 -146
- package/dist/bot/xvfb.js.map +0 -1
|
@@ -1,855 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, copyFileSync, cpSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { homedir, hostname, tmpdir } from "node:os";
|
|
4
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
-
import { CHROME_PROFILE_DIR, processBirthIdentity, processBirthIdentityState, profilePathIdentity, profileProcessIdentity, profileProcessIdentityState, signalProfileProcess, } from "./profile.js";
|
|
6
|
-
// Fixed per-namespace admission bound. These directories, rather than an
|
|
7
|
-
// in-memory counter, are the authority across MCP processes. A third operator
|
|
8
|
-
// start in the same namespace may wait at the provision seam, but can never
|
|
9
|
-
// create a third active profile while both slots are claimed.
|
|
10
|
-
const ACTIVE_SLOT_COUNT = 2;
|
|
11
|
-
const UNPUBLISHED_SEED_GENERATION = "unpublished";
|
|
12
|
-
const STARTUP_GRACE_MS = 30_000;
|
|
13
|
-
const WARM_IDLE_TTL_MS = 6 * 60 * 60 * 1_000;
|
|
14
|
-
const WARM_MAX_REUSES = 50;
|
|
15
|
-
const WARM_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
16
|
-
const TRANSIENT_PROFILE_NAMES = new Set([
|
|
17
|
-
"SingletonLock",
|
|
18
|
-
"SingletonSocket",
|
|
19
|
-
"SingletonCookie",
|
|
20
|
-
"DevToolsActivePort",
|
|
21
|
-
]);
|
|
22
|
-
const TRANSIENT_PROFILE_DIRS = new Set(["Cache", "Code Cache", "GPUCache", "Crashpad"]);
|
|
23
|
-
const IDENTITY_SEED_FILES = [
|
|
24
|
-
"Local State",
|
|
25
|
-
"logged-in-providers.json",
|
|
26
|
-
"provider-emails.json",
|
|
27
|
-
];
|
|
28
|
-
const POOLED_COOKIE_STORES = ["Default/Cookies", "Default/Network/Cookies"];
|
|
29
|
-
const SQLITE_SIDECAR_SUFFIXES = ["", "-journal", "-shm", "-wal"];
|
|
30
|
-
export const GOOGLE_LOGIN_COOKIE_MARKERS = ["__Secure-1PSID", "SAPISID", "SID"];
|
|
31
|
-
export class OperatorProfileAcquisitionInterruptedError extends Error {
|
|
32
|
-
reason;
|
|
33
|
-
phase;
|
|
34
|
-
constructor(reason, phase = "profile") {
|
|
35
|
-
super(`operator profile acquisition ${reason === "timeout" ? "timed out" : "was cancelled"}`);
|
|
36
|
-
this.reason = reason;
|
|
37
|
-
this.phase = phase;
|
|
38
|
-
this.name = "OperatorProfileAcquisitionInterruptedError";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function defaultPoolRoot() {
|
|
42
|
-
return process.env.VITEST === "true"
|
|
43
|
-
? join(tmpdir(), `trusty-squire-operator-profiles-test-${process.pid}`)
|
|
44
|
-
: join(homedir(), ".trusty-squire", "operator-profiles");
|
|
45
|
-
}
|
|
46
|
-
function poolRootForSource(sourceProfileDir, rootDir) {
|
|
47
|
-
if (rootDir !== undefined)
|
|
48
|
-
return resolve(rootDir);
|
|
49
|
-
const namespace = createHash("sha256")
|
|
50
|
-
.update(profilePathIdentity(sourceProfileDir))
|
|
51
|
-
.digest("hex")
|
|
52
|
-
.slice(0, 24);
|
|
53
|
-
return join(defaultPoolRoot(), "namespaces", namespace);
|
|
54
|
-
}
|
|
55
|
-
function paths(rootDir) {
|
|
56
|
-
const root = resolve(rootDir ?? defaultPoolRoot());
|
|
57
|
-
const seed = join(root, "seed");
|
|
58
|
-
return {
|
|
59
|
-
root,
|
|
60
|
-
seed,
|
|
61
|
-
generations: join(seed, "generations"),
|
|
62
|
-
current: join(seed, "current"),
|
|
63
|
-
seedLock: join(seed, ".lock"),
|
|
64
|
-
profiles: join(root, "profiles"),
|
|
65
|
-
active: join(root, "active"),
|
|
66
|
-
activeClaims: join(root, "active-claims"),
|
|
67
|
-
warm: join(root, "warm"),
|
|
68
|
-
tombstones: join(root, "tombstones"),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function ensurePrivateDir(path) {
|
|
72
|
-
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
73
|
-
chmodSync(path, 0o700);
|
|
74
|
-
}
|
|
75
|
-
function initializePool(p) {
|
|
76
|
-
for (const dir of [
|
|
77
|
-
p.root,
|
|
78
|
-
p.seed,
|
|
79
|
-
p.generations,
|
|
80
|
-
p.profiles,
|
|
81
|
-
p.active,
|
|
82
|
-
p.activeClaims,
|
|
83
|
-
p.warm,
|
|
84
|
-
p.tombstones,
|
|
85
|
-
]) {
|
|
86
|
-
ensurePrivateDir(dir);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function writePrivateJson(path, value) {
|
|
90
|
-
const temporary = `${path}.tmp.${randomUUID()}`;
|
|
91
|
-
try {
|
|
92
|
-
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
93
|
-
chmodSync(temporary, 0o600);
|
|
94
|
-
renameSync(temporary, path);
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
rmSync(temporary, { force: true });
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function readJson(path) {
|
|
102
|
-
try {
|
|
103
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function currentProcessIdentity() {
|
|
110
|
-
return processBirthIdentity(process.pid) ?? { pid: process.pid, start_time: "unknown" };
|
|
111
|
-
}
|
|
112
|
-
function processState(identity) {
|
|
113
|
-
return processBirthIdentityState(identity);
|
|
114
|
-
}
|
|
115
|
-
function readSeedLockOwner(path) {
|
|
116
|
-
try {
|
|
117
|
-
return lstatSync(path).isDirectory()
|
|
118
|
-
? readJson(join(path, "owner.json"))
|
|
119
|
-
: readJson(path);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function seedLockState(path) {
|
|
126
|
-
// Valid owner records are reclaimed only when their same-host PID/start_time is
|
|
127
|
-
// confirmed stale. A matching or cross-host owner is intentionally unreclaimed:
|
|
128
|
-
// wall-clock reclaim of a plausibly-live owner cannot be fully race-free without
|
|
129
|
-
// a heavier heartbeat or fencing protocol, which was rejected as out of scope.
|
|
130
|
-
// It also cannot protect against an already-running old-version orphan like the
|
|
131
|
-
// 2026-08-15 incident; identify and kill that process by PID instead. The
|
|
132
|
-
// startup grace below applies only when no valid owner record can be read.
|
|
133
|
-
const owner = readSeedLockOwner(path);
|
|
134
|
-
if (owner !== null) {
|
|
135
|
-
if (owner.host !== hostname())
|
|
136
|
-
return "unknown";
|
|
137
|
-
return processState(owner);
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
return Date.now() - lstatSync(path).mtimeMs >= STARTUP_GRACE_MS ? "stale" : "unknown";
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
return err.code === "ENOENT" ? "stale" : "unknown";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function seedLockArtifacts(p, kind) {
|
|
147
|
-
const dir = kind === "claim" ? p.seed : p.tombstones;
|
|
148
|
-
const prefix = kind === "claim" ? `${basename(p.seedLock)}.claim-` : "seed-lock-";
|
|
149
|
-
try {
|
|
150
|
-
return readdirSync(dir)
|
|
151
|
-
.filter((name) => name.startsWith(prefix))
|
|
152
|
-
.map((name) => join(dir, name));
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function scavengeSeedLockArtifacts(p) {
|
|
159
|
-
let retainedTombstone = false;
|
|
160
|
-
for (const tombstone of seedLockArtifacts(p, "stale")) {
|
|
161
|
-
if (seedLockState(tombstone) === "stale") {
|
|
162
|
-
rmSync(tombstone, { recursive: true, force: true });
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
retainedTombstone = true;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
for (const claim of seedLockArtifacts(p, "claim")) {
|
|
169
|
-
if (seedLockState(claim) === "stale")
|
|
170
|
-
rmSync(claim, { force: true });
|
|
171
|
-
}
|
|
172
|
-
return retainedTombstone;
|
|
173
|
-
}
|
|
174
|
-
function quarantineStaleSeedLock(p) {
|
|
175
|
-
if (seedLockState(p.seedLock) !== "stale")
|
|
176
|
-
return false;
|
|
177
|
-
const tombstone = join(p.tombstones, `seed-lock-${randomUUID()}`);
|
|
178
|
-
try {
|
|
179
|
-
renameSync(p.seedLock, tombstone);
|
|
180
|
-
}
|
|
181
|
-
catch (err) {
|
|
182
|
-
return err.code === "ENOENT";
|
|
183
|
-
}
|
|
184
|
-
if (seedLockState(tombstone) === "stale") {
|
|
185
|
-
rmSync(tombstone, { recursive: true, force: true });
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
function stripTransientProfileState(root) {
|
|
191
|
-
const walk = (dir) => {
|
|
192
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
193
|
-
const path = join(dir, entry.name);
|
|
194
|
-
if (TRANSIENT_PROFILE_NAMES.has(entry.name) || TRANSIENT_PROFILE_DIRS.has(entry.name)) {
|
|
195
|
-
rmSync(path, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
else if (entry.isDirectory()) {
|
|
198
|
-
walk(path);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
if (existsSync(root))
|
|
203
|
-
walk(root);
|
|
204
|
-
}
|
|
205
|
-
function stripPooledProfileCookies(root) {
|
|
206
|
-
for (const relative of POOLED_COOKIE_STORES) {
|
|
207
|
-
for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
|
|
208
|
-
rmSync(join(root, `${relative}${suffix}`), { force: true });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function copyIdentitySeedFile(sourceRoot, destinationRoot, relative) {
|
|
213
|
-
const source = join(sourceRoot, relative);
|
|
214
|
-
try {
|
|
215
|
-
if (!lstatSync(source).isFile())
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const destination = join(destinationRoot, relative);
|
|
222
|
-
ensurePrivateDir(dirname(destination));
|
|
223
|
-
copyFileSync(source, destination);
|
|
224
|
-
chmodSync(destination, 0o600);
|
|
225
|
-
}
|
|
226
|
-
// Pooled clones must never carry Google's session cookies: a clone presents
|
|
227
|
-
// already-rotated-away cookie values (Google rotates them on the live
|
|
228
|
-
// profile as it's used), and Google's server treats that as credential
|
|
229
|
-
// replay and invalidates the whole session family — killing the user's real
|
|
230
|
-
// Google session for a casual pooled run. Google identity now only flows
|
|
231
|
-
// through the direct, unshared profile (operator-direct-identity.ts, gated
|
|
232
|
-
// by operate_start's require_live_identity); the seed and its clones carry
|
|
233
|
-
// no cookies at all.
|
|
234
|
-
function copyIdentitySeed(sourceProfileDir, destination) {
|
|
235
|
-
if (!lstatSync(sourceProfileDir).isDirectory()) {
|
|
236
|
-
throw new Error(`operator login source is not a directory: ${sourceProfileDir}`);
|
|
237
|
-
}
|
|
238
|
-
ensurePrivateDir(destination);
|
|
239
|
-
for (const relative of IDENTITY_SEED_FILES) {
|
|
240
|
-
copyIdentitySeedFile(sourceProfileDir, destination, relative);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
function assertProfileAcquisitionActive(control, phase = "profile") {
|
|
244
|
-
if (control.signal?.aborted === true) {
|
|
245
|
-
throw new OperatorProfileAcquisitionInterruptedError("cancelled", phase);
|
|
246
|
-
}
|
|
247
|
-
if (control.deadline !== undefined && Date.now() >= control.deadline) {
|
|
248
|
-
throw new OperatorProfileAcquisitionInterruptedError("timeout", phase);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
async function waitForSeedLockRetry(control) {
|
|
252
|
-
assertProfileAcquisitionActive(control, "seed_lock");
|
|
253
|
-
const delay = control.deadline === undefined ? 25 : Math.max(0, Math.min(25, control.deadline - Date.now()));
|
|
254
|
-
await new Promise((resolveWait) => {
|
|
255
|
-
let settled = false;
|
|
256
|
-
let timer;
|
|
257
|
-
const finish = () => {
|
|
258
|
-
if (settled)
|
|
259
|
-
return;
|
|
260
|
-
settled = true;
|
|
261
|
-
if (timer !== undefined)
|
|
262
|
-
clearTimeout(timer);
|
|
263
|
-
control.signal?.removeEventListener("abort", finish);
|
|
264
|
-
resolveWait();
|
|
265
|
-
};
|
|
266
|
-
timer = setTimeout(finish, delay);
|
|
267
|
-
control.signal?.addEventListener("abort", finish, { once: true });
|
|
268
|
-
if (control.signal?.aborted === true)
|
|
269
|
-
finish();
|
|
270
|
-
});
|
|
271
|
-
assertProfileAcquisitionActive(control, "seed_lock");
|
|
272
|
-
}
|
|
273
|
-
async function withSeedLock(p, fn, control = {}) {
|
|
274
|
-
const token = randomUUID();
|
|
275
|
-
const owner = {
|
|
276
|
-
host: hostname(),
|
|
277
|
-
...currentProcessIdentity(),
|
|
278
|
-
token,
|
|
279
|
-
};
|
|
280
|
-
for (;;) {
|
|
281
|
-
assertProfileAcquisitionActive(control, "seed_lock");
|
|
282
|
-
if (scavengeSeedLockArtifacts(p)) {
|
|
283
|
-
await waitForSeedLockRetry(control);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const claim = `${p.seedLock}.claim-${randomUUID()}`;
|
|
287
|
-
writeFileSync(claim, `${JSON.stringify(owner)}\n`, { mode: 0o600, flag: "wx" });
|
|
288
|
-
try {
|
|
289
|
-
linkSync(claim, p.seedLock);
|
|
290
|
-
}
|
|
291
|
-
catch (err) {
|
|
292
|
-
rmSync(claim, { force: true });
|
|
293
|
-
if (err.code !== "EEXIST")
|
|
294
|
-
throw err;
|
|
295
|
-
if (quarantineStaleSeedLock(p))
|
|
296
|
-
continue;
|
|
297
|
-
await waitForSeedLockRetry(control);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
try {
|
|
301
|
-
rmSync(claim, { force: true });
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
if (readSeedLockOwner(p.seedLock)?.token === token) {
|
|
305
|
-
rmSync(p.seedLock, { recursive: true, force: true });
|
|
306
|
-
}
|
|
307
|
-
throw err;
|
|
308
|
-
}
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
try {
|
|
312
|
-
assertProfileAcquisitionActive(control, "seed_lock");
|
|
313
|
-
return await fn();
|
|
314
|
-
}
|
|
315
|
-
finally {
|
|
316
|
-
if (readSeedLockOwner(p.seedLock)?.token === token) {
|
|
317
|
-
rmSync(p.seedLock, { recursive: true, force: true });
|
|
318
|
-
}
|
|
319
|
-
for (const tombstone of seedLockArtifacts(p, "stale")) {
|
|
320
|
-
if (readSeedLockOwner(tombstone)?.token === token) {
|
|
321
|
-
rmSync(tombstone, { recursive: true, force: true });
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function currentGeneration(p) {
|
|
327
|
-
try {
|
|
328
|
-
const target = readlinkSync(p.current);
|
|
329
|
-
const generation = basename(target);
|
|
330
|
-
return existsSync(join(p.generations, generation, "user-data")) ? generation : null;
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function publishSeedLocked(p, sourceProfileDir) {
|
|
337
|
-
const generation = randomUUID();
|
|
338
|
-
const staging = join(p.generations, `.${generation}.tmp`);
|
|
339
|
-
const destination = join(p.generations, generation);
|
|
340
|
-
try {
|
|
341
|
-
ensurePrivateDir(staging);
|
|
342
|
-
copyIdentitySeed(sourceProfileDir, join(staging, "user-data"));
|
|
343
|
-
renameSync(staging, destination);
|
|
344
|
-
const nextLink = join(p.seed, `.current-${generation}`);
|
|
345
|
-
symlinkSync(join("generations", generation), nextLink, "dir");
|
|
346
|
-
renameSync(nextLink, p.current);
|
|
347
|
-
for (const entry of readdirSync(p.generations, { withFileTypes: true })) {
|
|
348
|
-
if (entry.name !== generation) {
|
|
349
|
-
rmSync(join(p.generations, entry.name), { recursive: true, force: true });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return generation;
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
rmSync(staging, { recursive: true, force: true });
|
|
356
|
-
throw err;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
export function assertOperatorProfileRuntimeSupported(platform = process.platform) {
|
|
360
|
-
if (platform !== "linux") {
|
|
361
|
-
throw new Error("operator profile leases require Linux process identity");
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
export function canPublishOperatorProfileSeed(proof, platform = process.platform) {
|
|
365
|
-
return (platform === "linux" &&
|
|
366
|
-
proof.provider === "google" &&
|
|
367
|
-
proof.loginStatus === "completed" &&
|
|
368
|
-
proof.closeState === "closed");
|
|
369
|
-
}
|
|
370
|
-
export async function publishOperatorProfileSeed(sourceProfileDir = CHROME_PROFILE_DIR, opts) {
|
|
371
|
-
if (!canPublishOperatorProfileSeed(opts.proof)) {
|
|
372
|
-
throw new Error("operator profile seed publication requires explicit completed Google login provenance and verified Linux Chrome closure");
|
|
373
|
-
}
|
|
374
|
-
const resolvedSource = profilePathIdentity(sourceProfileDir);
|
|
375
|
-
const p = paths(poolRootForSource(resolvedSource, opts.rootDir));
|
|
376
|
-
initializePool(p);
|
|
377
|
-
return await withSeedLock(p, () => publishSeedLocked(p, resolvedSource));
|
|
378
|
-
}
|
|
379
|
-
function profileDir(p, profileId) {
|
|
380
|
-
return join(p.profiles, profileId, "user-data");
|
|
381
|
-
}
|
|
382
|
-
function readLease(claimDir) {
|
|
383
|
-
const lease = readJson(join(claimDir, "lease.json"));
|
|
384
|
-
if (lease?.version !== 1 ||
|
|
385
|
-
typeof lease.lease_token !== "string" ||
|
|
386
|
-
typeof lease.profile_id !== "string" ||
|
|
387
|
-
!/^[0-9a-f-]{36}$/.test(lease.profile_id) ||
|
|
388
|
-
typeof lease.seed_generation !== "string" ||
|
|
389
|
-
typeof lease.created_at !== "number" ||
|
|
390
|
-
typeof lease.reuse_count !== "number" ||
|
|
391
|
-
(lease.disposition !== undefined && lease.disposition !== "destroy_required")) {
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
return lease;
|
|
395
|
-
}
|
|
396
|
-
function removeProfile(p, lease) {
|
|
397
|
-
rmSync(join(p.profiles, lease.profile_id), { recursive: true, force: true });
|
|
398
|
-
}
|
|
399
|
-
function quarantine(p, publicPath, label) {
|
|
400
|
-
const privatePath = join(p.tombstones, `${label}-${randomUUID()}`);
|
|
401
|
-
try {
|
|
402
|
-
renameSync(publicPath, privatePath);
|
|
403
|
-
return privatePath;
|
|
404
|
-
}
|
|
405
|
-
catch (err) {
|
|
406
|
-
if (["ENOENT", "EEXIST", "ENOTEMPTY"].includes(err.code ?? "")) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
throw err;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
function removeSlotContainer(p, path) {
|
|
413
|
-
let privateClaim = null;
|
|
414
|
-
try {
|
|
415
|
-
if (lstatSync(path).isSymbolicLink()) {
|
|
416
|
-
privateClaim = resolve(dirname(path), readlinkSync(path));
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
rmSync(path, { recursive: true, force: true });
|
|
423
|
-
if (privateClaim !== null && dirname(privateClaim) === p.activeClaims) {
|
|
424
|
-
rmSync(privateClaim, { recursive: true, force: true });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
function reserveActiveSlot(p, index, sessionId, beforePublish) {
|
|
428
|
-
const ownerToken = randomUUID();
|
|
429
|
-
const privateClaim = join(p.activeClaims, randomUUID());
|
|
430
|
-
const candidate = join(p.active, `slot-${index}`);
|
|
431
|
-
ensurePrivateDir(privateClaim);
|
|
432
|
-
let published = false;
|
|
433
|
-
try {
|
|
434
|
-
const identity = currentProcessIdentity();
|
|
435
|
-
writePrivateJson(join(privateClaim, "owner.json"), {
|
|
436
|
-
host: hostname(),
|
|
437
|
-
...identity,
|
|
438
|
-
token: ownerToken,
|
|
439
|
-
session_id: sessionId,
|
|
440
|
-
});
|
|
441
|
-
beforePublish?.();
|
|
442
|
-
try {
|
|
443
|
-
symlinkSync(relative(p.active, privateClaim), candidate, "dir");
|
|
444
|
-
}
|
|
445
|
-
catch (err) {
|
|
446
|
-
if (err.code === "EEXIST")
|
|
447
|
-
return null;
|
|
448
|
-
throw err;
|
|
449
|
-
}
|
|
450
|
-
published = true;
|
|
451
|
-
if (readJson(join(candidate, "owner.json"))?.token !== ownerToken) {
|
|
452
|
-
throw new Error("operator active-slot ownership was lost during reservation");
|
|
453
|
-
}
|
|
454
|
-
return { slotDir: candidate, ownerToken };
|
|
455
|
-
}
|
|
456
|
-
finally {
|
|
457
|
-
if (!published)
|
|
458
|
-
rmSync(privateClaim, { recursive: true, force: true });
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
function quarantineOwnedActiveSlot(p, slotDir, ownerToken, label) {
|
|
462
|
-
if (readJson(join(slotDir, "owner.json"))?.token !== ownerToken)
|
|
463
|
-
return null;
|
|
464
|
-
const tombstone = quarantine(p, slotDir, label);
|
|
465
|
-
if (tombstone === null)
|
|
466
|
-
return null;
|
|
467
|
-
if (readJson(join(tombstone, "owner.json"))?.token !== ownerToken) {
|
|
468
|
-
// The renamed directory is private, so retaining an ambiguity is safe.
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
return tombstone;
|
|
472
|
-
}
|
|
473
|
-
// removeProfile deletes a real, potentially large per-session Chrome profile
|
|
474
|
-
// (cache/history/IndexedDB accumulated over real browsing) — a disk-bound
|
|
475
|
-
// rmSync that can be genuinely slow. onRemovable, when supplied, lets the
|
|
476
|
-
// caller defer that physical delete until after the seed lock (which this
|
|
477
|
-
// function normally runs under, via scavengeQuarantinedActive /
|
|
478
|
-
// scavengeActiveSlots) has been released, so an unrelated concurrent
|
|
479
|
-
// operate_start never queues behind someone else's cleanup.
|
|
480
|
-
function reapQuarantinedActive(p, tombstone, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess, onRemovable) {
|
|
481
|
-
const owner = readJson(join(tombstone, "owner.json"));
|
|
482
|
-
if (owner !== null && (owner.host !== hostname() || processState(owner) !== "stale")) {
|
|
483
|
-
const slotIndex = /^active-(\d+)-/.exec(basename(tombstone))?.[1];
|
|
484
|
-
if (slotIndex === undefined)
|
|
485
|
-
return;
|
|
486
|
-
const publicSlot = join(p.active, `slot-${slotIndex}`);
|
|
487
|
-
try {
|
|
488
|
-
renameSync(tombstone, publicSlot);
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
// Another owner already reserved capacity. Keep this lease private and
|
|
492
|
-
// leak-safe for a later manual inspection rather than killing ambiguity.
|
|
493
|
-
}
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
const claim = join(tombstone, "claim");
|
|
497
|
-
const lease = readLease(claim);
|
|
498
|
-
if (lease === null) {
|
|
499
|
-
removeSlotContainer(p, tombstone);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const dir = profileDir(p, lease.profile_id);
|
|
503
|
-
const worker = lease.worker;
|
|
504
|
-
if (worker === undefined)
|
|
505
|
-
return;
|
|
506
|
-
const state = workerState(worker, dir);
|
|
507
|
-
if (state === "unknown")
|
|
508
|
-
return;
|
|
509
|
-
if (state === "matching") {
|
|
510
|
-
if (!signalWorker(worker, dir, "SIGKILL"))
|
|
511
|
-
return;
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
if (onRemovable) {
|
|
515
|
-
onRemovable({ lease, finish: () => removeSlotContainer(p, tombstone) });
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
removeProfile(p, lease);
|
|
519
|
-
removeSlotContainer(p, tombstone);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
function scavengeDestroyRequired(p, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess) {
|
|
523
|
-
for (const entry of readdirSync(p.tombstones)) {
|
|
524
|
-
if (!entry.startsWith("destroy-required-"))
|
|
525
|
-
continue;
|
|
526
|
-
const tombstone = join(p.tombstones, entry);
|
|
527
|
-
const lease = readLease(join(tombstone, "claim"));
|
|
528
|
-
if (lease?.disposition !== "destroy_required" || lease.worker === undefined)
|
|
529
|
-
continue;
|
|
530
|
-
const dir = profileDir(p, lease.profile_id);
|
|
531
|
-
const state = workerState(lease.worker, dir);
|
|
532
|
-
if (state === "unknown")
|
|
533
|
-
continue;
|
|
534
|
-
if (state === "matching") {
|
|
535
|
-
signalWorker(lease.worker, dir, "SIGKILL");
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
removeProfile(p, lease);
|
|
539
|
-
removeSlotContainer(p, tombstone);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function scavengeQuarantinedActive(p, workerState = profileProcessIdentityState, signalWorker = signalProfileProcess, onRemovable) {
|
|
543
|
-
for (const entry of readdirSync(p.tombstones)) {
|
|
544
|
-
if (!/^active-\d+-/.test(entry))
|
|
545
|
-
continue;
|
|
546
|
-
reapQuarantinedActive(p, join(p.tombstones, entry), workerState, signalWorker, onRemovable);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function scavengeActiveSlots(p, now, onRemovable) {
|
|
550
|
-
for (let index = 0; index < ACTIVE_SLOT_COUNT; index += 1) {
|
|
551
|
-
const slot = join(p.active, `slot-${index}`);
|
|
552
|
-
if (!existsSync(slot))
|
|
553
|
-
continue;
|
|
554
|
-
const owner = readJson(join(slot, "owner.json"));
|
|
555
|
-
if (owner === null) {
|
|
556
|
-
if (now - statSync(slot).mtimeMs < STARTUP_GRACE_MS)
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
else if (owner.host !== hostname() || processState(owner) !== "stale") {
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
// The public slot is only a candidate until this rename wins. No process is
|
|
563
|
-
// inspected again, signalled, or deleted while the lease remains claimable.
|
|
564
|
-
const tombstone = quarantine(p, slot, `active-${index}`);
|
|
565
|
-
if (tombstone !== null)
|
|
566
|
-
reapQuarantinedActive(p, tombstone, undefined, undefined, onRemovable);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
// onRemovable defers the physical delete of an expired warm profile's real
|
|
570
|
-
// (potentially large) directory to after the caller releases the seed lock —
|
|
571
|
-
// see reapQuarantinedActive's comment for why.
|
|
572
|
-
function scavengeWarm(p, generation, now, onRemovable) {
|
|
573
|
-
const removeQuarantinedWarm = (tombstone) => {
|
|
574
|
-
const lease = readLease(tombstone);
|
|
575
|
-
if (lease === null) {
|
|
576
|
-
rmSync(tombstone, { recursive: true, force: true });
|
|
577
|
-
}
|
|
578
|
-
else if (onRemovable) {
|
|
579
|
-
onRemovable({
|
|
580
|
-
lease,
|
|
581
|
-
finish: () => rmSync(tombstone, { recursive: true, force: true }),
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
removeProfile(p, lease);
|
|
586
|
-
rmSync(tombstone, { recursive: true, force: true });
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
for (const entry of readdirSync(p.tombstones)) {
|
|
590
|
-
if (entry.startsWith("warm-"))
|
|
591
|
-
removeQuarantinedWarm(join(p.tombstones, entry));
|
|
592
|
-
}
|
|
593
|
-
const warm = join(p.warm, "slot-0");
|
|
594
|
-
if (!existsSync(warm))
|
|
595
|
-
return;
|
|
596
|
-
const lease = readLease(warm);
|
|
597
|
-
const expired = lease === null ||
|
|
598
|
-
lease.worker !== undefined ||
|
|
599
|
-
lease.seed_generation !== generation ||
|
|
600
|
-
lease.returned_at === undefined ||
|
|
601
|
-
now - lease.returned_at >= WARM_IDLE_TTL_MS ||
|
|
602
|
-
now - lease.created_at >= WARM_MAX_AGE_MS ||
|
|
603
|
-
lease.reuse_count >= WARM_MAX_REUSES;
|
|
604
|
-
if (!expired)
|
|
605
|
-
return;
|
|
606
|
-
const tombstone = quarantine(p, warm, "warm");
|
|
607
|
-
if (tombstone === null)
|
|
608
|
-
return;
|
|
609
|
-
removeQuarantinedWarm(tombstone);
|
|
610
|
-
}
|
|
611
|
-
function flushDeferredProfileRemovals(p, removals) {
|
|
612
|
-
for (const removal of removals) {
|
|
613
|
-
try {
|
|
614
|
-
removeProfile(p, removal.lease);
|
|
615
|
-
removal.finish();
|
|
616
|
-
}
|
|
617
|
-
catch { }
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
export class OperatorProfileLease {
|
|
621
|
-
p;
|
|
622
|
-
slotDir;
|
|
623
|
-
claimDir;
|
|
624
|
-
ownerToken;
|
|
625
|
-
descriptor;
|
|
626
|
-
now;
|
|
627
|
-
profileDir;
|
|
628
|
-
profileId;
|
|
629
|
-
seedGeneration;
|
|
630
|
-
finished = false;
|
|
631
|
-
constructor(p, slotDir, claimDir, ownerToken, descriptor, now) {
|
|
632
|
-
this.p = p;
|
|
633
|
-
this.slotDir = slotDir;
|
|
634
|
-
this.claimDir = claimDir;
|
|
635
|
-
this.ownerToken = ownerToken;
|
|
636
|
-
this.descriptor = descriptor;
|
|
637
|
-
this.now = now;
|
|
638
|
-
this.profileId = descriptor.profile_id;
|
|
639
|
-
this.seedGeneration = descriptor.seed_generation;
|
|
640
|
-
this.profileDir = profileDir(p, descriptor.profile_id);
|
|
641
|
-
}
|
|
642
|
-
bindWorker(worker) {
|
|
643
|
-
if (this.finished)
|
|
644
|
-
throw new Error("operator profile lease is already released");
|
|
645
|
-
if (worker.host !== hostname() ||
|
|
646
|
-
profilePathIdentity(worker.user_data_dir) !== profilePathIdentity(this.profileDir)) {
|
|
647
|
-
throw new Error("operator worker identity does not match its leased profile");
|
|
648
|
-
}
|
|
649
|
-
this.descriptor = { ...this.descriptor, worker };
|
|
650
|
-
writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
|
|
651
|
-
}
|
|
652
|
-
async returnWarm(closeState) {
|
|
653
|
-
if (this.finished)
|
|
654
|
-
return;
|
|
655
|
-
if (this.descriptor.disposition === "destroy_required") {
|
|
656
|
-
if (closeState === "closed")
|
|
657
|
-
await this.destroy();
|
|
658
|
-
else
|
|
659
|
-
await this.retain(true);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (closeState !== "closed") {
|
|
663
|
-
await this.retain();
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const withoutWorker = { ...this.descriptor };
|
|
667
|
-
delete withoutWorker.worker;
|
|
668
|
-
this.descriptor = {
|
|
669
|
-
...withoutWorker,
|
|
670
|
-
returned_at: this.now(),
|
|
671
|
-
reuse_count: this.descriptor.reuse_count + 1,
|
|
672
|
-
};
|
|
673
|
-
writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
|
|
674
|
-
const current = currentGeneration(this.p);
|
|
675
|
-
const warm = join(this.p.warm, "slot-0");
|
|
676
|
-
let pooled = current === this.descriptor.seed_generation;
|
|
677
|
-
if (pooled) {
|
|
678
|
-
try {
|
|
679
|
-
renameSync(this.claimDir, warm);
|
|
680
|
-
}
|
|
681
|
-
catch (err) {
|
|
682
|
-
if (!["EEXIST", "ENOTEMPTY"].includes(err.code ?? ""))
|
|
683
|
-
throw err;
|
|
684
|
-
pooled = false;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
if (!pooled) {
|
|
688
|
-
this.discardOwnedSlot();
|
|
689
|
-
this.finished = true;
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
this.releaseSlot();
|
|
693
|
-
this.finished = true;
|
|
694
|
-
}
|
|
695
|
-
async destroy() {
|
|
696
|
-
if (this.finished)
|
|
697
|
-
return;
|
|
698
|
-
this.descriptor = { ...this.descriptor, disposition: "destroy_required" };
|
|
699
|
-
writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
|
|
700
|
-
this.discardOwnedSlot("destroy-required");
|
|
701
|
-
this.finished = true;
|
|
702
|
-
}
|
|
703
|
-
async retain(destroyRequired = false) {
|
|
704
|
-
if (this.finished)
|
|
705
|
-
return;
|
|
706
|
-
if (destroyRequired || this.descriptor.disposition === "destroy_required") {
|
|
707
|
-
this.descriptor = { ...this.descriptor, disposition: "destroy_required" };
|
|
708
|
-
writePrivateJson(join(this.claimDir, "lease.json"), this.descriptor);
|
|
709
|
-
}
|
|
710
|
-
quarantineOwnedActiveSlot(this.p, this.slotDir, this.ownerToken, this.descriptor.disposition === "destroy_required" ? "destroy-required" : "unverified-close");
|
|
711
|
-
this.finished = true;
|
|
712
|
-
}
|
|
713
|
-
discardOwnedSlot(label = "owned-active") {
|
|
714
|
-
const tombstone = quarantineOwnedActiveSlot(this.p, this.slotDir, this.ownerToken, label);
|
|
715
|
-
if (tombstone === null)
|
|
716
|
-
return;
|
|
717
|
-
const quarantinedLease = readLease(join(tombstone, "claim"));
|
|
718
|
-
if (quarantinedLease?.lease_token !== this.descriptor.lease_token) {
|
|
719
|
-
// Ownership changed or the descriptor became ambiguous. The private
|
|
720
|
-
// tombstone is no longer claimable, so retain it without deleting data.
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
removeProfile(this.p, quarantinedLease);
|
|
724
|
-
removeSlotContainer(this.p, tombstone);
|
|
725
|
-
}
|
|
726
|
-
releaseSlot() {
|
|
727
|
-
const owner = readJson(join(this.slotDir, "owner.json"));
|
|
728
|
-
if (owner?.token === this.ownerToken)
|
|
729
|
-
removeSlotContainer(this.p, this.slotDir);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
export async function acquireOperatorProfile(sessionId, opts = {}) {
|
|
733
|
-
assertOperatorProfileRuntimeSupported();
|
|
734
|
-
const sourceProfileDir = profilePathIdentity(opts.sourceProfileDir ?? CHROME_PROFILE_DIR);
|
|
735
|
-
const p = paths(poolRootForSource(sourceProfileDir, opts.rootDir));
|
|
736
|
-
const now = opts.now ?? Date.now;
|
|
737
|
-
initializePool(p);
|
|
738
|
-
scavengeDestroyRequired(p);
|
|
739
|
-
// Collected by scavengeQuarantinedActive/scavengeActiveSlots instead of
|
|
740
|
-
// physically removed inline, so the (potentially slow, disk-bound) removal
|
|
741
|
-
// of a real per-session profile directory happens after the seed lock
|
|
742
|
-
// below is released, never while it's held.
|
|
743
|
-
const reclaimedFromReservation = [];
|
|
744
|
-
const reservation = await withSeedLock(p, () => {
|
|
745
|
-
scavengeQuarantinedActive(p, undefined, undefined, (removal) => reclaimedFromReservation.push(removal));
|
|
746
|
-
scavengeActiveSlots(p, now(), (removal) => reclaimedFromReservation.push(removal));
|
|
747
|
-
for (let index = 0; index < ACTIVE_SLOT_COUNT; index += 1) {
|
|
748
|
-
const claimed = reserveActiveSlot(p, index, sessionId);
|
|
749
|
-
if (claimed !== null)
|
|
750
|
-
return claimed;
|
|
751
|
-
}
|
|
752
|
-
return null;
|
|
753
|
-
}, opts);
|
|
754
|
-
flushDeferredProfileRemovals(p, reclaimedFromReservation);
|
|
755
|
-
if (reservation === null) {
|
|
756
|
-
throw new Error("operate_start capacity reached: 2 operator sessions are active; finish one and retry");
|
|
757
|
-
}
|
|
758
|
-
const { slotDir, ownerToken } = reservation;
|
|
759
|
-
const claimDir = join(slotDir, "claim");
|
|
760
|
-
let allocatedProfileId = null;
|
|
761
|
-
// Same deferred-removal reasoning as reclaimedFromReservation above, for
|
|
762
|
-
// the expired-warm-profile case surfaced while cloning/reusing.
|
|
763
|
-
const reclaimedFromWarm = [];
|
|
764
|
-
try {
|
|
765
|
-
assertProfileAcquisitionActive(opts);
|
|
766
|
-
const acquiredDescriptor = await withSeedLock(p, () => {
|
|
767
|
-
const generation = currentGeneration(p);
|
|
768
|
-
scavengeWarm(p, generation ?? UNPUBLISHED_SEED_GENERATION, now(), (removal) => reclaimedFromWarm.push(removal));
|
|
769
|
-
const warm = join(p.warm, "slot-0");
|
|
770
|
-
let descriptor = null;
|
|
771
|
-
try {
|
|
772
|
-
renameSync(warm, claimDir);
|
|
773
|
-
descriptor = readLease(claimDir);
|
|
774
|
-
}
|
|
775
|
-
catch (err) {
|
|
776
|
-
if (err.code !== "ENOENT")
|
|
777
|
-
throw err;
|
|
778
|
-
}
|
|
779
|
-
if (descriptor !== null) {
|
|
780
|
-
stripPooledProfileCookies(profileDir(p, descriptor.profile_id));
|
|
781
|
-
const activeDescriptor = { ...descriptor };
|
|
782
|
-
delete activeDescriptor.returned_at;
|
|
783
|
-
delete activeDescriptor.worker;
|
|
784
|
-
writePrivateJson(join(claimDir, "lease.json"), activeDescriptor);
|
|
785
|
-
return activeDescriptor;
|
|
786
|
-
}
|
|
787
|
-
rmSync(claimDir, { recursive: true, force: true });
|
|
788
|
-
const profileId = randomUUID();
|
|
789
|
-
allocatedProfileId = profileId;
|
|
790
|
-
const profileRoot = join(p.profiles, profileId);
|
|
791
|
-
ensurePrivateDir(profileRoot);
|
|
792
|
-
const userDataDir = join(profileRoot, "user-data");
|
|
793
|
-
if (generation === null) {
|
|
794
|
-
ensurePrivateDir(userDataDir);
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
cpSync(join(p.generations, generation, "user-data"), userDataDir, {
|
|
798
|
-
recursive: true,
|
|
799
|
-
force: false,
|
|
800
|
-
errorOnExist: true,
|
|
801
|
-
});
|
|
802
|
-
stripTransientProfileState(userDataDir);
|
|
803
|
-
}
|
|
804
|
-
stripPooledProfileCookies(userDataDir);
|
|
805
|
-
ensurePrivateDir(claimDir);
|
|
806
|
-
const coldDescriptor = {
|
|
807
|
-
version: 1,
|
|
808
|
-
lease_token: randomUUID(),
|
|
809
|
-
profile_id: profileId,
|
|
810
|
-
seed_generation: generation ?? UNPUBLISHED_SEED_GENERATION,
|
|
811
|
-
created_at: now(),
|
|
812
|
-
reuse_count: 0,
|
|
813
|
-
};
|
|
814
|
-
writePrivateJson(join(claimDir, "lease.json"), coldDescriptor);
|
|
815
|
-
return coldDescriptor;
|
|
816
|
-
}, opts);
|
|
817
|
-
assertProfileAcquisitionActive(opts);
|
|
818
|
-
return new OperatorProfileLease(p, slotDir, claimDir, ownerToken, acquiredDescriptor, now);
|
|
819
|
-
}
|
|
820
|
-
catch (err) {
|
|
821
|
-
const tombstone = quarantineOwnedActiveSlot(p, slotDir, ownerToken, "failed-active");
|
|
822
|
-
if (tombstone !== null) {
|
|
823
|
-
const lease = readLease(join(tombstone, "claim"));
|
|
824
|
-
let descriptorWasSafe = false;
|
|
825
|
-
if (lease !== null) {
|
|
826
|
-
removeProfile(p, lease);
|
|
827
|
-
descriptorWasSafe = true;
|
|
828
|
-
}
|
|
829
|
-
else if (allocatedProfileId !== null) {
|
|
830
|
-
rmSync(join(p.profiles, allocatedProfileId), { recursive: true, force: true });
|
|
831
|
-
descriptorWasSafe = true;
|
|
832
|
-
}
|
|
833
|
-
if (descriptorWasSafe)
|
|
834
|
-
removeSlotContainer(p, tombstone);
|
|
835
|
-
}
|
|
836
|
-
throw err;
|
|
837
|
-
}
|
|
838
|
-
finally {
|
|
839
|
-
flushDeferredProfileRemovals(p, reclaimedFromWarm);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
export function localWorkerIdentity(pid, userDataDir) {
|
|
843
|
-
return profileProcessIdentity(pid, userDataDir);
|
|
844
|
-
}
|
|
845
|
-
export const operatorProfilePoolTest = {
|
|
846
|
-
paths,
|
|
847
|
-
poolRootForSource,
|
|
848
|
-
currentGeneration,
|
|
849
|
-
withSeedLock,
|
|
850
|
-
reserveActiveSlot,
|
|
851
|
-
scavengeQuarantinedActive,
|
|
852
|
-
scavengeDestroyRequired,
|
|
853
|
-
resetDefaultPool: () => rmSync(defaultPoolRoot(), { recursive: true, force: true }),
|
|
854
|
-
};
|
|
855
|
-
//# sourceMappingURL=operator-profile-pool.js.map
|