@trusty-squire/mcp 1.1.13-rc.9 → 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 +136 -64
- 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 +85 -80
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1314 -1570
- 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 +18 -6
- package/dist/bot/operator-browser-watchdog.d.ts.map +1 -1
- package/dist/bot/operator-browser-watchdog.js +91 -37
- package/dist/bot/operator-browser-watchdog.js.map +1 -1
- 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 +7 -1
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +269 -103
- 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 +57 -186
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +1976 -1812
- 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 +151 -96
- package/dist/tools/operate-pay.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +47 -39
- 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
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { operatorBrowserProcessCommandState, operatorBrowserProcessMarkerState, } from "./operator-browser-watchdog.js";
|
|
9
|
+
import { processBirthIdentity, processBirthIdentityState, processProfileArgumentState, profilePathIdentity, profileProcessIdentityState, signalProfileProcess, } from "./profile.js";
|
|
10
|
+
export const OWNER_HELPER_MARKER_ENV = "TRUSTY_SQUIRE_OWNER_HELPER_MARKER";
|
|
11
|
+
export const OWNER_REAPER_WORKER_MARKER_ENV = "TRUSTY_SQUIRE_OWNER_REAPER_WORKER_MARKER";
|
|
12
|
+
const DEFAULT_POLL_MS = 1_000;
|
|
13
|
+
const DEFAULT_TERM_GRACE_MS = 2_000;
|
|
14
|
+
const DEFAULT_WORKER_READY_TIMEOUT_MS = 2_000;
|
|
15
|
+
const DEFAULT_WORKER_STABILITY_MS = 100;
|
|
16
|
+
const WORKER_RECOVERY_DELAY_MS = 100;
|
|
17
|
+
const workerReadyWait = new Int32Array(new SharedArrayBuffer(4));
|
|
18
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
19
|
+
let activeReaper = null;
|
|
20
|
+
function defaultRootDir() {
|
|
21
|
+
const configured = (process.env.TRUSTY_SQUIRE_REAPER_DIR ?? "").trim();
|
|
22
|
+
return configured.length > 0
|
|
23
|
+
? resolve(configured)
|
|
24
|
+
: join(homedir(), ".trusty-squire", "owner-reapers");
|
|
25
|
+
}
|
|
26
|
+
function envPositiveMs(name, fallback) {
|
|
27
|
+
const parsed = Number(process.env[name]);
|
|
28
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
29
|
+
}
|
|
30
|
+
function ensurePrivateDir(path) {
|
|
31
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
32
|
+
chmodSync(path, 0o700);
|
|
33
|
+
}
|
|
34
|
+
function isOwnerIdentity(value) {
|
|
35
|
+
if (value === null || typeof value !== "object")
|
|
36
|
+
return false;
|
|
37
|
+
const owner = value;
|
|
38
|
+
return (Number.isSafeInteger(owner.pid) && (owner.pid ?? 0) > 0 && typeof owner.start_time === "string");
|
|
39
|
+
}
|
|
40
|
+
function isLaunch(value) {
|
|
41
|
+
if (value === null || typeof value !== "object")
|
|
42
|
+
return false;
|
|
43
|
+
const launch = value;
|
|
44
|
+
const allowedKeys = new Set(["marker", "user_data_dir", "anchor"]);
|
|
45
|
+
return (Object.keys(launch).every((key) => allowedKeys.has(key)) &&
|
|
46
|
+
typeof launch.marker === "string" &&
|
|
47
|
+
typeof launch.user_data_dir === "string" &&
|
|
48
|
+
(launch.anchor === undefined || isOwnerIdentity(launch.anchor)));
|
|
49
|
+
}
|
|
50
|
+
function isProfileProcessIdentity(value) {
|
|
51
|
+
if (value === null || typeof value !== "object")
|
|
52
|
+
return false;
|
|
53
|
+
const identity = value;
|
|
54
|
+
return (typeof identity.host === "string" &&
|
|
55
|
+
Number.isSafeInteger(identity.pid) &&
|
|
56
|
+
(identity.pid ?? 0) > 0 &&
|
|
57
|
+
typeof identity.start_time === "string" &&
|
|
58
|
+
typeof identity.user_data_dir === "string" &&
|
|
59
|
+
(identity.process_group_id === undefined ||
|
|
60
|
+
identity.process_group_id === "unknown" ||
|
|
61
|
+
Number.isSafeInteger(identity.process_group_id)) &&
|
|
62
|
+
(identity.process_marker === undefined || typeof identity.process_marker === "string"));
|
|
63
|
+
}
|
|
64
|
+
function isHelper(value) {
|
|
65
|
+
if (value === null || typeof value !== "object")
|
|
66
|
+
return false;
|
|
67
|
+
const helper = value;
|
|
68
|
+
if (helper.state === "pending") {
|
|
69
|
+
return (typeof helper.marker === "string" &&
|
|
70
|
+
helper.pid === undefined &&
|
|
71
|
+
helper.start_time === undefined &&
|
|
72
|
+
helper.process_group_id === undefined);
|
|
73
|
+
}
|
|
74
|
+
return (Number.isSafeInteger(helper.pid) &&
|
|
75
|
+
(helper.pid ?? 0) > 0 &&
|
|
76
|
+
typeof helper.start_time === "string" &&
|
|
77
|
+
typeof helper.marker === "string" &&
|
|
78
|
+
(helper.process_group_id === undefined || Number.isSafeInteger(helper.process_group_id)));
|
|
79
|
+
}
|
|
80
|
+
function readManifest(path) {
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
83
|
+
const allowedKeys = new Set(["version", "token", "owner", "resources", "launches", "helpers"]);
|
|
84
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key)) ||
|
|
85
|
+
value.version !== 5 ||
|
|
86
|
+
typeof value.token !== "string" ||
|
|
87
|
+
!isOwnerIdentity(value.owner) ||
|
|
88
|
+
!Array.isArray(value.resources) ||
|
|
89
|
+
!value.resources.every(isProfileProcessIdentity) ||
|
|
90
|
+
!Array.isArray(value.launches) ||
|
|
91
|
+
!value.launches.every(isLaunch) ||
|
|
92
|
+
!Array.isArray(value.helpers) ||
|
|
93
|
+
!value.helpers.every(isHelper)) {
|
|
94
|
+
return { state: "unknown" };
|
|
95
|
+
}
|
|
96
|
+
return { state: "present", manifest: value };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return error.code === "ENOENT"
|
|
100
|
+
? { state: "missing" }
|
|
101
|
+
: { state: "unknown" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function writeManifest(path, manifest) {
|
|
105
|
+
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
106
|
+
try {
|
|
107
|
+
writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: "wx" });
|
|
108
|
+
renameSync(temporary, path);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
rmSync(temporary, { force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function shouldReapOwner(state) {
|
|
115
|
+
return state === "stale";
|
|
116
|
+
}
|
|
117
|
+
function ownerState(manifest) {
|
|
118
|
+
const running = linuxProcessRunningState(manifest.owner.pid);
|
|
119
|
+
return running === "matching" ? processBirthIdentityState(manifest.owner) : running;
|
|
120
|
+
}
|
|
121
|
+
function linuxProcessGroupId(pid) {
|
|
122
|
+
try {
|
|
123
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
124
|
+
const closeParen = stat.lastIndexOf(")");
|
|
125
|
+
if (closeParen < 0)
|
|
126
|
+
return "unknown";
|
|
127
|
+
const processGroupId = Number(stat
|
|
128
|
+
.slice(closeParen + 2)
|
|
129
|
+
.trim()
|
|
130
|
+
.split(/\s+/)[2]);
|
|
131
|
+
return Number.isSafeInteger(processGroupId) && processGroupId > 0 ? processGroupId : "unknown";
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const code = error.code;
|
|
135
|
+
return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function linuxProcessUidState(pid) {
|
|
139
|
+
const ownerUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
140
|
+
if (ownerUid === null)
|
|
141
|
+
return "unknown";
|
|
142
|
+
try {
|
|
143
|
+
const match = /^Uid:\s+(.+)$/m.exec(readFileSync(`/proc/${pid}/status`, "utf8"));
|
|
144
|
+
if (match === null)
|
|
145
|
+
return "unknown";
|
|
146
|
+
const uids = match[1].trim().split(/\s+/).map(Number).filter(Number.isSafeInteger);
|
|
147
|
+
return uids.includes(ownerUid) ? "matching" : "stale";
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const code = error.code;
|
|
151
|
+
return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function linuxProcessRunningState(pid) {
|
|
155
|
+
try {
|
|
156
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
157
|
+
const close = stat.lastIndexOf(")");
|
|
158
|
+
if (close < 0)
|
|
159
|
+
return "unknown";
|
|
160
|
+
const state = stat.slice(close + 2).split(" ")[0];
|
|
161
|
+
return state === "Z" || state === "X" ? "stale" : "matching";
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const code = error.code;
|
|
165
|
+
return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function helperProcessMarkerState(pid, marker, readEnvironment = (processId) => readFileSync(`/proc/${processId}/environ`, "utf8")) {
|
|
169
|
+
try {
|
|
170
|
+
return readEnvironment(pid).split("\0").includes(`${OWNER_HELPER_MARKER_ENV}=${marker}`)
|
|
171
|
+
? "matching"
|
|
172
|
+
: "stale";
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const code = error.code;
|
|
176
|
+
return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function processIds(readProcessIds) {
|
|
180
|
+
return (readProcessIds?.() ??
|
|
181
|
+
readdirSync("/proc")
|
|
182
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
183
|
+
.map(Number));
|
|
184
|
+
}
|
|
185
|
+
function helperGroupMarkerState(processGroupId, marker, readers = {}) {
|
|
186
|
+
if (process.platform !== "linux" && readers.readProcessIds === undefined)
|
|
187
|
+
return "stale";
|
|
188
|
+
let unknown = false;
|
|
189
|
+
try {
|
|
190
|
+
for (const pid of processIds(readers.readProcessIds)) {
|
|
191
|
+
const groupId = (readers.readProcessGroupId ?? linuxProcessGroupId)(pid);
|
|
192
|
+
if (groupId === "unknown") {
|
|
193
|
+
const running = readers.readRunningState?.(pid) ??
|
|
194
|
+
(readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
|
|
195
|
+
if (running === "stale")
|
|
196
|
+
continue;
|
|
197
|
+
const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
|
|
198
|
+
if (markerState === "matching" ||
|
|
199
|
+
(markerState === "unknown" &&
|
|
200
|
+
(readers.readUidState ?? linuxProcessUidState)(pid) !== "stale")) {
|
|
201
|
+
unknown = true;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (groupId === "stale" || groupId !== processGroupId)
|
|
206
|
+
continue;
|
|
207
|
+
const running = readers.readRunningState?.(pid) ??
|
|
208
|
+
(readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
|
|
209
|
+
if (running === "stale")
|
|
210
|
+
continue;
|
|
211
|
+
if (running === "unknown") {
|
|
212
|
+
unknown = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
|
|
216
|
+
if (markerState === "matching")
|
|
217
|
+
return "matching";
|
|
218
|
+
if (markerState === "unknown")
|
|
219
|
+
unknown = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return "unknown";
|
|
224
|
+
}
|
|
225
|
+
return unknown ? "unknown" : "stale";
|
|
226
|
+
}
|
|
227
|
+
function exactHelperMarkerProcessScan(marker, readers = {}) {
|
|
228
|
+
if (process.platform !== "linux" && readers.readProcessIds === undefined) {
|
|
229
|
+
return { matching: [], unknown: false };
|
|
230
|
+
}
|
|
231
|
+
const matching = [];
|
|
232
|
+
let unknown = false;
|
|
233
|
+
try {
|
|
234
|
+
for (const pid of processIds(readers.readProcessIds)) {
|
|
235
|
+
const running = readers.readRunningState?.(pid) ??
|
|
236
|
+
(readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
|
|
237
|
+
if (running === "stale")
|
|
238
|
+
continue;
|
|
239
|
+
if (running === "unknown") {
|
|
240
|
+
if ((readers.readUidState ?? linuxProcessUidState)(pid) !== "stale")
|
|
241
|
+
unknown = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
|
|
245
|
+
if (markerState === "matching")
|
|
246
|
+
matching.push(pid);
|
|
247
|
+
else if (markerState === "unknown" &&
|
|
248
|
+
(readers.readUidState ?? linuxProcessUidState)(pid) !== "stale") {
|
|
249
|
+
unknown = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
unknown = true;
|
|
255
|
+
}
|
|
256
|
+
return { matching, unknown };
|
|
257
|
+
}
|
|
258
|
+
export function ownerHelperIdentityState(identity, readers = {}) {
|
|
259
|
+
if ("state" in identity) {
|
|
260
|
+
const scan = exactHelperMarkerProcessScan(identity.marker, readers);
|
|
261
|
+
if (scan.matching.length > 0)
|
|
262
|
+
return "matching";
|
|
263
|
+
return scan.unknown ? "unknown" : "stale";
|
|
264
|
+
}
|
|
265
|
+
if (identity.process_group_id !== undefined) {
|
|
266
|
+
const groupState = helperGroupMarkerState(identity.process_group_id, identity.marker, readers);
|
|
267
|
+
if (groupState !== "stale")
|
|
268
|
+
return groupState;
|
|
269
|
+
}
|
|
270
|
+
const running = readers.readRunningState?.(identity.pid) ??
|
|
271
|
+
(readers.readBirthState === undefined ? linuxProcessRunningState(identity.pid) : "matching");
|
|
272
|
+
if (running !== "matching")
|
|
273
|
+
return running;
|
|
274
|
+
const birth = readers.readBirthState?.(identity) ?? processBirthIdentityState(identity);
|
|
275
|
+
if (birth !== "matching")
|
|
276
|
+
return birth;
|
|
277
|
+
return (readers.readMarkerState ?? helperProcessMarkerState)(identity.pid, identity.marker);
|
|
278
|
+
}
|
|
279
|
+
function signalOwnerHelper(identity, signal) {
|
|
280
|
+
if (process.platform !== "linux")
|
|
281
|
+
return false;
|
|
282
|
+
if ("state" in identity) {
|
|
283
|
+
let signalled = false;
|
|
284
|
+
const groups = new Set();
|
|
285
|
+
for (const pid of exactHelperMarkerProcessScan(identity.marker).matching) {
|
|
286
|
+
const groupId = linuxProcessGroupId(pid);
|
|
287
|
+
if (typeof groupId === "number" &&
|
|
288
|
+
helperGroupMarkerState(groupId, identity.marker) === "matching") {
|
|
289
|
+
groups.add(groupId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const groupId of groups) {
|
|
293
|
+
try {
|
|
294
|
+
process.kill(-groupId, signal);
|
|
295
|
+
signalled = true;
|
|
296
|
+
}
|
|
297
|
+
catch { }
|
|
298
|
+
}
|
|
299
|
+
return signalled;
|
|
300
|
+
}
|
|
301
|
+
const groupId = identity.process_group_id;
|
|
302
|
+
const target = groupId !== undefined && helperGroupMarkerState(groupId, identity.marker) === "matching"
|
|
303
|
+
? -groupId
|
|
304
|
+
: ownerHelperIdentityState(identity) === "matching"
|
|
305
|
+
? identity.pid
|
|
306
|
+
: null;
|
|
307
|
+
if (target === null)
|
|
308
|
+
return false;
|
|
309
|
+
try {
|
|
310
|
+
process.kill(target, signal);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function browserMarkerState(pid, readers) {
|
|
318
|
+
if (readers.readMarkerState !== undefined)
|
|
319
|
+
return readers.readMarkerState(pid);
|
|
320
|
+
if (readers.readMarker !== undefined) {
|
|
321
|
+
const marker = readers.readMarker(pid);
|
|
322
|
+
return marker === null ? { state: "missing" } : { state: "present", marker };
|
|
323
|
+
}
|
|
324
|
+
return operatorBrowserProcessMarkerState(pid);
|
|
325
|
+
}
|
|
326
|
+
function launchAnchorTrustState(launch, readers) {
|
|
327
|
+
if (launch.anchor === undefined)
|
|
328
|
+
return "pending";
|
|
329
|
+
const runningState = (readers.readRunningState ?? linuxProcessRunningState)(launch.anchor.pid);
|
|
330
|
+
if (runningState === "unknown")
|
|
331
|
+
return "unknown";
|
|
332
|
+
if (runningState === "stale")
|
|
333
|
+
return "trusted";
|
|
334
|
+
const birthState = readers.readBirthState?.(launch.anchor) ?? processBirthIdentityState(launch.anchor);
|
|
335
|
+
if (birthState === "unknown")
|
|
336
|
+
return "unknown";
|
|
337
|
+
if (birthState === "stale")
|
|
338
|
+
return "trusted";
|
|
339
|
+
const commandState = (readers.readCommandState ?? operatorBrowserProcessCommandState)(launch.anchor.pid);
|
|
340
|
+
const markerState = browserMarkerState(launch.anchor.pid, readers);
|
|
341
|
+
const profileState = (readers.readProfileState ?? processProfileArgumentState)(launch.anchor.pid, launch.user_data_dir);
|
|
342
|
+
// The anchor is a birth identity (pid + start_time) recorded at this launch,
|
|
343
|
+
// so we are already looking at THIS launch's process. Chrome overwrites its
|
|
344
|
+
// own /proc/<pid>/environ memory when it rewrites process titles, which erases
|
|
345
|
+
// the owner marker there — so a live chromium anchor is NEVER re-readable by
|
|
346
|
+
// marker even though it is genuinely ours. Its per-launch --user-data-dir is a
|
|
347
|
+
// unique ownership token that survives, so a matching chromium command plus a
|
|
348
|
+
// matching profile is definitive. A marker that IS still readable must not
|
|
349
|
+
// positively name a different launch; a missing/erased marker does not.
|
|
350
|
+
const markerContradicts = markerState.state === "present" && markerState.marker !== launch.marker;
|
|
351
|
+
return commandState === "matching" && profileState === "matching" && !markerContradicts
|
|
352
|
+
? "trusted"
|
|
353
|
+
: "unknown";
|
|
354
|
+
}
|
|
355
|
+
function exactMarkerProcessScan(launch, readers = {}) {
|
|
356
|
+
if (process.platform !== "linux" && readers.readProcessIds === undefined) {
|
|
357
|
+
return { matching: [], unknown: false };
|
|
358
|
+
}
|
|
359
|
+
const matching = [];
|
|
360
|
+
const missingProfile = [];
|
|
361
|
+
const anchorTrust = launchAnchorTrustState(launch, readers);
|
|
362
|
+
let discoveredAnchor;
|
|
363
|
+
let unknown = anchorTrust === "unknown";
|
|
364
|
+
try {
|
|
365
|
+
for (const pid of processIds(readers.readProcessIds)) {
|
|
366
|
+
const commandState = (readers.readCommandState ?? operatorBrowserProcessCommandState)(pid);
|
|
367
|
+
if (commandState === "stale")
|
|
368
|
+
continue;
|
|
369
|
+
const markerState = browserMarkerState(pid, readers);
|
|
370
|
+
if (markerState.state === "unknown") {
|
|
371
|
+
// Only a CONFIRMED chromium process (commandState "matching") whose
|
|
372
|
+
// marker we transiently cannot read is conservatively treated as
|
|
373
|
+
// maybe-ours. A process we cannot even confirm is chromium — e.g. a
|
|
374
|
+
// hardened own-uid process such as sshd, whose /proc/<pid>/exe and
|
|
375
|
+
// environ are both EACCES, so commandState reads "unknown" — cannot be
|
|
376
|
+
// our marked browser: our own login browser launches unhardened under
|
|
377
|
+
// our uid with a readable environ. Poisoning the scan on such a process
|
|
378
|
+
// made every login-browser teardown report closure unproven.
|
|
379
|
+
if (commandState === "matching" &&
|
|
380
|
+
(readers.readUidState ?? linuxProcessUidState)(pid) !== "stale") {
|
|
381
|
+
unknown = true;
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (markerState.state !== "present" || markerState.marker !== launch.marker)
|
|
386
|
+
continue;
|
|
387
|
+
if (commandState !== "matching") {
|
|
388
|
+
unknown = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const profileState = (readers.readProfileState ?? processProfileArgumentState)(pid, launch.user_data_dir);
|
|
392
|
+
if (profileState !== "matching" && profileState !== "missing") {
|
|
393
|
+
unknown = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const identity = (readers.readBirthIdentity ?? processBirthIdentity)(pid);
|
|
397
|
+
if (identity === null) {
|
|
398
|
+
if ((readers.readRunningState ?? linuxProcessRunningState)(pid) !== "stale")
|
|
399
|
+
unknown = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const birthState = readers.readBirthState?.(identity) ?? processBirthIdentityState(identity);
|
|
403
|
+
if (birthState !== "matching") {
|
|
404
|
+
if (birthState === "unknown")
|
|
405
|
+
unknown = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (profileState === "matching") {
|
|
409
|
+
matching.push(identity);
|
|
410
|
+
discoveredAnchor ??= identity;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
missingProfile.push(identity);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
unknown = true;
|
|
419
|
+
}
|
|
420
|
+
const missingProfileTrusted = anchorTrust === "trusted" || (anchorTrust === "pending" && discoveredAnchor !== undefined);
|
|
421
|
+
if (missingProfileTrusted)
|
|
422
|
+
matching.push(...missingProfile);
|
|
423
|
+
else if (missingProfile.length > 0)
|
|
424
|
+
unknown = true;
|
|
425
|
+
return {
|
|
426
|
+
matching,
|
|
427
|
+
unknown,
|
|
428
|
+
...(launch.anchor === undefined && discoveredAnchor !== undefined
|
|
429
|
+
? { anchor: discoveredAnchor }
|
|
430
|
+
: {}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function exactMarkerIdentityMatches(launch, identity, readers = {}) {
|
|
434
|
+
const scan = exactMarkerProcessScan(launch, {
|
|
435
|
+
...readers,
|
|
436
|
+
readProcessIds: () => [identity.pid],
|
|
437
|
+
});
|
|
438
|
+
return scan.matching.some((current) => current.pid === identity.pid && current.start_time === identity.start_time);
|
|
439
|
+
}
|
|
440
|
+
export function ownerBrowserLaunchState(marker, profileDir, readers = {}) {
|
|
441
|
+
const launch = {
|
|
442
|
+
marker,
|
|
443
|
+
user_data_dir: profilePathIdentity(profileDir),
|
|
444
|
+
...(readers.anchor === undefined ? {} : { anchor: readers.anchor }),
|
|
445
|
+
};
|
|
446
|
+
const scan = exactMarkerProcessScan(launch, readers);
|
|
447
|
+
if (scan.matching.length > 0)
|
|
448
|
+
return "matching";
|
|
449
|
+
return scan.unknown ? "unknown" : "stale";
|
|
450
|
+
}
|
|
451
|
+
export async function terminateOwnerBrowserLaunch(marker, profileDir, options = {}) {
|
|
452
|
+
const anchor = options.anchor ?? activeReaper?.launchAnchor(marker, profileDir) ?? undefined;
|
|
453
|
+
const readers = {
|
|
454
|
+
...(anchor === undefined ? {} : { anchor }),
|
|
455
|
+
...(options.readProcessIds === undefined ? {} : { readProcessIds: options.readProcessIds }),
|
|
456
|
+
...(options.readBirthIdentity === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { readBirthIdentity: options.readBirthIdentity }),
|
|
459
|
+
...(options.readMarkerState === undefined ? {} : { readMarkerState: options.readMarkerState }),
|
|
460
|
+
...(options.readCommandState === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: { readCommandState: options.readCommandState }),
|
|
463
|
+
...(options.readProfileState === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: { readProfileState: options.readProfileState }),
|
|
466
|
+
...(options.readBirthState === undefined ? {} : { readBirthState: options.readBirthState }),
|
|
467
|
+
...(options.readRunningState === undefined
|
|
468
|
+
? {}
|
|
469
|
+
: { readRunningState: options.readRunningState }),
|
|
470
|
+
...(options.readUidState === undefined ? {} : { readUidState: options.readUidState }),
|
|
471
|
+
};
|
|
472
|
+
const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
473
|
+
const wait = options.wait ??
|
|
474
|
+
(async (ms) => await new Promise((resolveWait) => setTimeout(resolveWait, ms)));
|
|
475
|
+
const graceMs = options.graceMs ?? envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
|
|
476
|
+
const launch = {
|
|
477
|
+
marker,
|
|
478
|
+
user_data_dir: profilePathIdentity(profileDir),
|
|
479
|
+
...(anchor === undefined ? {} : { anchor }),
|
|
480
|
+
};
|
|
481
|
+
const scan = () => {
|
|
482
|
+
const result = exactMarkerProcessScan(launch, readers);
|
|
483
|
+
if (launch.anchor === undefined && result.anchor !== undefined)
|
|
484
|
+
launch.anchor = result.anchor;
|
|
485
|
+
return result;
|
|
486
|
+
};
|
|
487
|
+
const matching = () => scan().matching;
|
|
488
|
+
const signal = (identities, signalName) => {
|
|
489
|
+
for (const identity of identities) {
|
|
490
|
+
if (!exactMarkerIdentityMatches(launch, identity, readers))
|
|
491
|
+
continue;
|
|
492
|
+
try {
|
|
493
|
+
kill(identity.pid, signalName);
|
|
494
|
+
}
|
|
495
|
+
catch { }
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const initial = matching();
|
|
499
|
+
if (initial.length === 0) {
|
|
500
|
+
return scan().unknown === false;
|
|
501
|
+
}
|
|
502
|
+
signal(initial, "SIGTERM");
|
|
503
|
+
await wait(graceMs);
|
|
504
|
+
const resistant = matching();
|
|
505
|
+
if (resistant.length === 0) {
|
|
506
|
+
return scan().unknown === false;
|
|
507
|
+
}
|
|
508
|
+
signal(resistant, "SIGKILL");
|
|
509
|
+
await wait(graceMs);
|
|
510
|
+
return matching().length === 0 && scan().unknown === false;
|
|
511
|
+
}
|
|
512
|
+
function signalTrackedResources(manifest, signal) {
|
|
513
|
+
let signalled = 0;
|
|
514
|
+
for (const identity of manifest.resources) {
|
|
515
|
+
if (signalProfileProcess(identity, identity.user_data_dir, signal))
|
|
516
|
+
signalled += 1;
|
|
517
|
+
}
|
|
518
|
+
for (const launch of manifest.launches) {
|
|
519
|
+
const scan = exactMarkerProcessScan(launch);
|
|
520
|
+
if (launch.anchor === undefined && scan.anchor !== undefined)
|
|
521
|
+
launch.anchor = scan.anchor;
|
|
522
|
+
for (const identity of scan.matching) {
|
|
523
|
+
if (!exactMarkerIdentityMatches(launch, identity))
|
|
524
|
+
continue;
|
|
525
|
+
try {
|
|
526
|
+
process.kill(identity.pid, signal);
|
|
527
|
+
signalled += 1;
|
|
528
|
+
}
|
|
529
|
+
catch { }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const helper of manifest.helpers) {
|
|
533
|
+
if (signalOwnerHelper(helper, signal))
|
|
534
|
+
signalled += 1;
|
|
535
|
+
}
|
|
536
|
+
return signalled;
|
|
537
|
+
}
|
|
538
|
+
function manifestCleanupComplete(manifest) {
|
|
539
|
+
return (manifest.resources.every((identity) => profileProcessIdentityState(identity, identity.user_data_dir) === "stale") &&
|
|
540
|
+
manifest.launches.every((launch) => {
|
|
541
|
+
const scan = exactMarkerProcessScan(launch);
|
|
542
|
+
return scan.matching.length === 0 && !scan.unknown;
|
|
543
|
+
}) &&
|
|
544
|
+
manifest.helpers.every((helper) => ownerHelperIdentityState(helper) === "stale"));
|
|
545
|
+
}
|
|
546
|
+
async function reapManifest(path, manifest) {
|
|
547
|
+
const graceMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
|
|
548
|
+
const signalled = signalTrackedResources(manifest, "SIGTERM");
|
|
549
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, graceMs));
|
|
550
|
+
const latestRead = readManifest(path);
|
|
551
|
+
const latest = latestRead.state === "present" ? latestRead.manifest : manifest;
|
|
552
|
+
for (const launch of latest.launches) {
|
|
553
|
+
if (launch.anchor !== undefined)
|
|
554
|
+
continue;
|
|
555
|
+
const prior = manifest.launches.find((candidate) => candidate.marker === launch.marker && candidate.user_data_dir === launch.user_data_dir);
|
|
556
|
+
if (prior?.anchor !== undefined)
|
|
557
|
+
launch.anchor = prior.anchor;
|
|
558
|
+
}
|
|
559
|
+
signalTrackedResources(latest, "SIGKILL");
|
|
560
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, graceMs));
|
|
561
|
+
if (latestRead.state === "present" && manifestCleanupComplete(latest)) {
|
|
562
|
+
rmSync(path, { force: true });
|
|
563
|
+
}
|
|
564
|
+
return signalled;
|
|
565
|
+
}
|
|
566
|
+
export async function sweepOrphanedOwnerProcesses(rootDir = defaultRootDir()) {
|
|
567
|
+
await new Promise((resolveStart) => setImmediate(resolveStart));
|
|
568
|
+
if (process.platform !== "linux" || !existsSync(rootDir))
|
|
569
|
+
return 0;
|
|
570
|
+
let reaped = 0;
|
|
571
|
+
for (const entry of readdirSync(rootDir)) {
|
|
572
|
+
if (!entry.endsWith(".json"))
|
|
573
|
+
continue;
|
|
574
|
+
const path = join(rootDir, entry);
|
|
575
|
+
const read = readManifest(path);
|
|
576
|
+
if (read.state !== "present" || !shouldReapOwner(ownerState(read.manifest)))
|
|
577
|
+
continue;
|
|
578
|
+
reaped += await reapManifest(path, read.manifest);
|
|
579
|
+
}
|
|
580
|
+
return reaped;
|
|
581
|
+
}
|
|
582
|
+
export async function runOwnerProcessReaperWorker(manifestPath, onReady = () => undefined) {
|
|
583
|
+
const pollMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_POLL_MS", DEFAULT_POLL_MS);
|
|
584
|
+
let ready = false;
|
|
585
|
+
for (;;) {
|
|
586
|
+
const read = readManifest(manifestPath);
|
|
587
|
+
if (read.state === "missing")
|
|
588
|
+
return;
|
|
589
|
+
if (read.state === "unknown") {
|
|
590
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, pollMs));
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (!ready) {
|
|
594
|
+
onReady();
|
|
595
|
+
ready = true;
|
|
596
|
+
}
|
|
597
|
+
if (shouldReapOwner(ownerState(read.manifest))) {
|
|
598
|
+
await reapManifest(manifestPath, read.manifest);
|
|
599
|
+
if (readManifest(manifestPath).state === "missing")
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, pollMs));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function ownerReaperWorkerRunning(identity) {
|
|
606
|
+
if (processBirthIdentityState(identity) !== "matching")
|
|
607
|
+
return false;
|
|
608
|
+
return linuxProcessRunningState(identity.pid) === "matching";
|
|
609
|
+
}
|
|
610
|
+
function waitForOwnerReaperWorker(worker, readyPath, token) {
|
|
611
|
+
if (worker.pid === undefined)
|
|
612
|
+
return null;
|
|
613
|
+
const timeoutMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_READY_TIMEOUT_MS", DEFAULT_WORKER_READY_TIMEOUT_MS);
|
|
614
|
+
const deadline = Date.now() + timeoutMs;
|
|
615
|
+
while (Date.now() < deadline) {
|
|
616
|
+
try {
|
|
617
|
+
const ready = JSON.parse(readFileSync(readyPath, "utf8"));
|
|
618
|
+
if (ready.version === 1 && ready.token === token && ready.pid === worker.pid) {
|
|
619
|
+
const identity = processBirthIdentity(worker.pid);
|
|
620
|
+
if (identity === null)
|
|
621
|
+
return null;
|
|
622
|
+
Atomics.wait(workerReadyWait, 0, 0, DEFAULT_WORKER_STABILITY_MS);
|
|
623
|
+
return ownerReaperWorkerRunning(identity) ? identity : null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch { }
|
|
627
|
+
Atomics.wait(workerReadyWait, 0, 0, 5);
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
function terminateOwnerHelperAfterRegistrationFailure(child, record) {
|
|
632
|
+
const graceMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
|
|
633
|
+
signalOwnerHelper(record, "SIGTERM");
|
|
634
|
+
Atomics.wait(workerReadyWait, 0, 0, graceMs);
|
|
635
|
+
if (ownerHelperIdentityState(record) !== "stale") {
|
|
636
|
+
signalOwnerHelper(record, "SIGKILL");
|
|
637
|
+
Atomics.wait(workerReadyWait, 0, 0, graceMs);
|
|
638
|
+
}
|
|
639
|
+
return ownerTrackedHelperState(child) === "stale" || ownerHelperIdentityState(record) === "stale";
|
|
640
|
+
}
|
|
641
|
+
const trackedHelperProcesses = new WeakMap();
|
|
642
|
+
function launchOwnerReaperWorker(workerPath, manifestPath, rootDir, token, spawnWorker, onUnexpectedExit) {
|
|
643
|
+
const workerExecArgs = workerPath.endsWith(".ts")
|
|
644
|
+
? ["--import", moduleRequire.resolve("tsx")]
|
|
645
|
+
: [];
|
|
646
|
+
const readyPath = join(rootDir, `${process.pid}-${token}-${randomUUID()}.ready`);
|
|
647
|
+
const workerMarker = `v1:reaper:${token}:${randomUUID()}`;
|
|
648
|
+
let worker;
|
|
649
|
+
try {
|
|
650
|
+
worker = spawnWorker(process.execPath, [...workerExecArgs, workerPath, manifestPath, readyPath, token], {
|
|
651
|
+
detached: true,
|
|
652
|
+
stdio: "ignore",
|
|
653
|
+
env: {
|
|
654
|
+
...process.env,
|
|
655
|
+
[OWNER_HELPER_MARKER_ENV]: workerMarker,
|
|
656
|
+
[OWNER_REAPER_WORKER_MARKER_ENV]: workerMarker,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
worker.unref();
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
rmSync(readyPath, { force: true });
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
let workerRecord = { marker: workerMarker, state: "pending" };
|
|
666
|
+
if (worker.pid !== undefined) {
|
|
667
|
+
const birth = processBirthIdentity(worker.pid);
|
|
668
|
+
const processGroupId = linuxProcessGroupId(worker.pid);
|
|
669
|
+
if (birth !== null && typeof processGroupId === "number") {
|
|
670
|
+
workerRecord = { ...birth, marker: workerMarker, process_group_id: processGroupId };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
trackedHelperProcesses.set(worker, workerRecord);
|
|
674
|
+
let exited = false;
|
|
675
|
+
let ready = false;
|
|
676
|
+
let stopping = false;
|
|
677
|
+
const markExited = () => {
|
|
678
|
+
if (exited)
|
|
679
|
+
return;
|
|
680
|
+
exited = true;
|
|
681
|
+
if (ready && !stopping)
|
|
682
|
+
onUnexpectedExit();
|
|
683
|
+
};
|
|
684
|
+
worker.once("error", markExited);
|
|
685
|
+
worker.once("exit", markExited);
|
|
686
|
+
const identity = waitForOwnerReaperWorker(worker, readyPath, token);
|
|
687
|
+
rmSync(readyPath, { force: true });
|
|
688
|
+
if (identity === null || exited) {
|
|
689
|
+
stopping = true;
|
|
690
|
+
if (!terminateOwnerHelperAfterRegistrationFailure(worker, workerRecord)) {
|
|
691
|
+
throw new Error("owner process reaper worker group did not terminate");
|
|
692
|
+
}
|
|
693
|
+
trackedHelperProcesses.delete(worker);
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
ready = true;
|
|
697
|
+
return {
|
|
698
|
+
isAvailable: () => !exited && ownerReaperWorkerRunning(identity),
|
|
699
|
+
stop: () => {
|
|
700
|
+
stopping = true;
|
|
701
|
+
exited = true;
|
|
702
|
+
const terminated = terminateOwnerHelperAfterRegistrationFailure(worker, workerRecord);
|
|
703
|
+
if (terminated)
|
|
704
|
+
trackedHelperProcesses.delete(worker);
|
|
705
|
+
return terminated;
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
export function startOwnerProcessReaper(options = {}, runtime = {}) {
|
|
710
|
+
if (process.platform !== "linux")
|
|
711
|
+
return null;
|
|
712
|
+
if (activeReaper !== null)
|
|
713
|
+
return activeReaper.restart() ? activeReaper : null;
|
|
714
|
+
const rootDir = resolve(options.rootDir ?? defaultRootDir());
|
|
715
|
+
ensurePrivateDir(rootDir);
|
|
716
|
+
const startupSweep = setImmediate(() => {
|
|
717
|
+
void sweepOrphanedOwnerProcesses(rootDir).catch(() => undefined);
|
|
718
|
+
});
|
|
719
|
+
startupSweep.unref();
|
|
720
|
+
const owner = processBirthIdentity(process.pid);
|
|
721
|
+
if (owner === null)
|
|
722
|
+
return null;
|
|
723
|
+
const token = randomUUID();
|
|
724
|
+
const manifestPath = join(rootDir, `${owner.pid}-${token}.json`);
|
|
725
|
+
let manifest = {
|
|
726
|
+
version: 5,
|
|
727
|
+
token,
|
|
728
|
+
owner,
|
|
729
|
+
resources: [],
|
|
730
|
+
launches: [],
|
|
731
|
+
helpers: [],
|
|
732
|
+
};
|
|
733
|
+
writeManifest(manifestPath, manifest);
|
|
734
|
+
const compiledWorkerPath = fileURLToPath(new URL("./owner-process-reaper-worker.js", import.meta.url));
|
|
735
|
+
const sourceWorkerPath = fileURLToPath(new URL("./owner-process-reaper-worker.ts", import.meta.url));
|
|
736
|
+
const workerPath = options.workerPath ?? (existsSync(compiledWorkerPath) ? compiledWorkerPath : sourceWorkerPath);
|
|
737
|
+
if (!existsSync(workerPath)) {
|
|
738
|
+
rmSync(manifestPath, { force: true });
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const spawnWorker = runtime.spawn ?? spawn;
|
|
742
|
+
let stopped = false;
|
|
743
|
+
let workerGeneration = 0;
|
|
744
|
+
let worker = null;
|
|
745
|
+
let recoveryTimer = null;
|
|
746
|
+
const clearRecoveryTimer = () => {
|
|
747
|
+
if (recoveryTimer === null)
|
|
748
|
+
return;
|
|
749
|
+
clearTimeout(recoveryTimer);
|
|
750
|
+
recoveryTimer = null;
|
|
751
|
+
};
|
|
752
|
+
const scheduleRecovery = () => {
|
|
753
|
+
if (stopped || recoveryTimer !== null)
|
|
754
|
+
return;
|
|
755
|
+
recoveryTimer = setTimeout(() => {
|
|
756
|
+
recoveryTimer = null;
|
|
757
|
+
if (stopped || worker?.isAvailable())
|
|
758
|
+
return;
|
|
759
|
+
worker = null;
|
|
760
|
+
try {
|
|
761
|
+
launchWorker();
|
|
762
|
+
}
|
|
763
|
+
catch { }
|
|
764
|
+
if (worker === null)
|
|
765
|
+
scheduleRecovery();
|
|
766
|
+
}, WORKER_RECOVERY_DELAY_MS);
|
|
767
|
+
recoveryTimer.unref();
|
|
768
|
+
};
|
|
769
|
+
const launchWorker = () => {
|
|
770
|
+
runtime.beforeWorkerLaunch?.();
|
|
771
|
+
const generation = ++workerGeneration;
|
|
772
|
+
const launched = launchOwnerReaperWorker(workerPath, manifestPath, rootDir, token, spawnWorker, () => {
|
|
773
|
+
if (generation !== workerGeneration)
|
|
774
|
+
return;
|
|
775
|
+
worker = null;
|
|
776
|
+
scheduleRecovery();
|
|
777
|
+
});
|
|
778
|
+
if (launched !== null)
|
|
779
|
+
worker = launched;
|
|
780
|
+
return launched;
|
|
781
|
+
};
|
|
782
|
+
const ensureWorker = () => {
|
|
783
|
+
if (stopped)
|
|
784
|
+
return false;
|
|
785
|
+
if (worker?.isAvailable())
|
|
786
|
+
return true;
|
|
787
|
+
clearRecoveryTimer();
|
|
788
|
+
worker?.stop();
|
|
789
|
+
worker = null;
|
|
790
|
+
try {
|
|
791
|
+
return launchWorker() !== null;
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
scheduleRecovery();
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
if (!ensureWorker()) {
|
|
799
|
+
stopped = true;
|
|
800
|
+
clearRecoveryTimer();
|
|
801
|
+
rmSync(manifestPath, { force: true });
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const requireAvailable = () => {
|
|
805
|
+
if (!ensureWorker())
|
|
806
|
+
throw new Error("owner process reaper worker unavailable");
|
|
807
|
+
};
|
|
808
|
+
const update = (next) => {
|
|
809
|
+
manifest = next;
|
|
810
|
+
writeManifest(manifestPath, manifest);
|
|
811
|
+
};
|
|
812
|
+
const reaper = {
|
|
813
|
+
manifestPath,
|
|
814
|
+
token,
|
|
815
|
+
isAvailable: () => !stopped && worker?.isAvailable() === true,
|
|
816
|
+
restart: ensureWorker,
|
|
817
|
+
track: (identity) => {
|
|
818
|
+
requireAvailable();
|
|
819
|
+
update({
|
|
820
|
+
...manifest,
|
|
821
|
+
resources: [...manifest.resources.filter((entry) => entry.pid !== identity.pid), identity],
|
|
822
|
+
});
|
|
823
|
+
},
|
|
824
|
+
untrack: (identity) => {
|
|
825
|
+
if (stopped)
|
|
826
|
+
return;
|
|
827
|
+
update({
|
|
828
|
+
...manifest,
|
|
829
|
+
resources: manifest.resources.filter((entry) => entry.pid !== identity.pid || entry.start_time !== identity.start_time),
|
|
830
|
+
});
|
|
831
|
+
},
|
|
832
|
+
trackLaunch: (marker, profileDir) => {
|
|
833
|
+
requireAvailable();
|
|
834
|
+
update({
|
|
835
|
+
...manifest,
|
|
836
|
+
launches: [
|
|
837
|
+
...manifest.launches.filter((entry) => entry.marker !== marker),
|
|
838
|
+
{ marker, user_data_dir: profilePathIdentity(profileDir) },
|
|
839
|
+
],
|
|
840
|
+
});
|
|
841
|
+
},
|
|
842
|
+
bindLaunch: (marker, identity) => {
|
|
843
|
+
requireAvailable();
|
|
844
|
+
const launch = manifest.launches.find((entry) => entry.marker === marker);
|
|
845
|
+
// Ownership is proven by the birth-identity + unique per-launch profile
|
|
846
|
+
// (profileProcessIdentityState "matching"), a random per-launch
|
|
847
|
+
// --user-data-dir no other process shares. The environ marker is only a
|
|
848
|
+
// non-contradiction check here: Chrome erases its own marker from
|
|
849
|
+
// /proc/<pid>/environ when it rewrites process titles, so requiring the
|
|
850
|
+
// marker to still be readable back from the browser process left the
|
|
851
|
+
// anchor permanently unbound and every login-browser teardown reporting
|
|
852
|
+
// closure unproven. A marker that IS readable must not name a different
|
|
853
|
+
// launch; an erased marker does not disqualify.
|
|
854
|
+
const processMarker = operatorBrowserProcessMarkerState(identity.pid);
|
|
855
|
+
if (launch === undefined ||
|
|
856
|
+
profilePathIdentity(identity.user_data_dir) !== launch.user_data_dir ||
|
|
857
|
+
profileProcessIdentityState(identity, launch.user_data_dir) !== "matching" ||
|
|
858
|
+
(processMarker.state === "present" && processMarker.marker !== marker)) {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
update({
|
|
862
|
+
...manifest,
|
|
863
|
+
launches: manifest.launches.map((entry) => entry.marker === marker
|
|
864
|
+
? { ...entry, anchor: { pid: identity.pid, start_time: identity.start_time } }
|
|
865
|
+
: entry),
|
|
866
|
+
});
|
|
867
|
+
return true;
|
|
868
|
+
},
|
|
869
|
+
launchAnchor: (marker, profileDir) => {
|
|
870
|
+
const normalizedProfile = profilePathIdentity(profileDir);
|
|
871
|
+
return (manifest.launches.find((entry) => entry.marker === marker && entry.user_data_dir === normalizedProfile)?.anchor ?? null);
|
|
872
|
+
},
|
|
873
|
+
untrackLaunch: (marker) => {
|
|
874
|
+
if (stopped)
|
|
875
|
+
return;
|
|
876
|
+
update({
|
|
877
|
+
...manifest,
|
|
878
|
+
launches: manifest.launches.filter((entry) => entry.marker !== marker),
|
|
879
|
+
});
|
|
880
|
+
},
|
|
881
|
+
reserveHelper: (marker) => {
|
|
882
|
+
requireAvailable();
|
|
883
|
+
update({
|
|
884
|
+
...manifest,
|
|
885
|
+
helpers: [
|
|
886
|
+
...manifest.helpers.filter((entry) => entry.marker !== marker),
|
|
887
|
+
{ marker, state: "pending" },
|
|
888
|
+
],
|
|
889
|
+
});
|
|
890
|
+
},
|
|
891
|
+
bindHelper: (marker, identity) => {
|
|
892
|
+
requireAvailable();
|
|
893
|
+
if (!manifest.helpers.some((entry) => entry.marker === marker))
|
|
894
|
+
return;
|
|
895
|
+
update({
|
|
896
|
+
...manifest,
|
|
897
|
+
helpers: [...manifest.helpers.filter((entry) => entry.marker !== marker), identity],
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
untrackHelper: (marker) => {
|
|
901
|
+
if (stopped)
|
|
902
|
+
return;
|
|
903
|
+
update({
|
|
904
|
+
...manifest,
|
|
905
|
+
helpers: manifest.helpers.filter((entry) => entry.marker !== marker),
|
|
906
|
+
});
|
|
907
|
+
},
|
|
908
|
+
stop: () => {
|
|
909
|
+
if (stopped)
|
|
910
|
+
return;
|
|
911
|
+
stopped = true;
|
|
912
|
+
clearRecoveryTimer();
|
|
913
|
+
const stoppedWorker = worker?.stop() ?? true;
|
|
914
|
+
worker = null;
|
|
915
|
+
if (stoppedWorker)
|
|
916
|
+
rmSync(manifestPath, { force: true });
|
|
917
|
+
if (activeReaper === reaper)
|
|
918
|
+
activeReaper = null;
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
activeReaper = reaper;
|
|
922
|
+
return reaper;
|
|
923
|
+
}
|
|
924
|
+
export function ensureOwnerProcessReaper() {
|
|
925
|
+
if (activeReaper !== null)
|
|
926
|
+
return activeReaper.restart() ? activeReaper : null;
|
|
927
|
+
return startOwnerProcessReaper();
|
|
928
|
+
}
|
|
929
|
+
export function stopOwnerProcessReaper() {
|
|
930
|
+
activeReaper?.stop();
|
|
931
|
+
}
|
|
932
|
+
export function trackOwnerProcess(identity) {
|
|
933
|
+
ensureOwnerProcessReaper()?.track(identity);
|
|
934
|
+
}
|
|
935
|
+
export function untrackOwnerProcess(identity) {
|
|
936
|
+
activeReaper?.untrack(identity);
|
|
937
|
+
}
|
|
938
|
+
export function trackOwnerBrowserLaunch(marker, profileDir, runtime = {}) {
|
|
939
|
+
const reaper = (runtime.ensureReaper ?? ensureOwnerProcessReaper)();
|
|
940
|
+
if (process.platform === "linux" && reaper === null) {
|
|
941
|
+
throw new Error("owner process reaper unavailable for local browser launch");
|
|
942
|
+
}
|
|
943
|
+
reaper?.trackLaunch(marker, profileDir);
|
|
944
|
+
}
|
|
945
|
+
export function bindOwnerBrowserLaunch(marker, identity) {
|
|
946
|
+
if (process.platform !== "linux")
|
|
947
|
+
return true;
|
|
948
|
+
return activeReaper?.bindLaunch(marker, identity) === true;
|
|
949
|
+
}
|
|
950
|
+
// Launch markers remain tracked until exact-marker cleanup completes. Linux has
|
|
951
|
+
// one custodian: the owner reaper. This compatibility hook intentionally does
|
|
952
|
+
// not arm a second process watchdog.
|
|
953
|
+
export function markOwnerBrowserLaunchTerminal(_marker) { }
|
|
954
|
+
export function untrackOwnerBrowserLaunch(marker) {
|
|
955
|
+
activeReaper?.untrackLaunch(marker);
|
|
956
|
+
}
|
|
957
|
+
export function reconcileOwnerBrowserLaunchAfterLeaderExit(marker, profileDir, launchState = ownerBrowserLaunchState) {
|
|
958
|
+
if (launchState(marker, profileDir) === "stale")
|
|
959
|
+
untrackOwnerBrowserLaunch(marker);
|
|
960
|
+
}
|
|
961
|
+
export function spawnOwnerTrackedHelper(command, args, options = {}, runtime = {}) {
|
|
962
|
+
const reaper = ensureOwnerProcessReaper();
|
|
963
|
+
if (process.platform === "linux" && reaper === null) {
|
|
964
|
+
throw new Error("owner process reaper unavailable for session helper");
|
|
965
|
+
}
|
|
966
|
+
const marker = `v1:${randomUUID()}`;
|
|
967
|
+
reaper?.reserveHelper(marker);
|
|
968
|
+
let child;
|
|
969
|
+
try {
|
|
970
|
+
child = (runtime.spawn ?? spawn)(command, [...args], {
|
|
971
|
+
...options,
|
|
972
|
+
detached: process.platform === "linux" ? true : options.detached,
|
|
973
|
+
env: {
|
|
974
|
+
...(options.env ?? process.env),
|
|
975
|
+
[OWNER_HELPER_MARKER_ENV]: marker,
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
reaper?.untrackHelper(marker);
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
if (process.platform === "linux") {
|
|
984
|
+
let record = { marker, state: "pending" };
|
|
985
|
+
if (child.pid !== undefined) {
|
|
986
|
+
const birth = processBirthIdentity(child.pid);
|
|
987
|
+
const processGroupId = linuxProcessGroupId(child.pid);
|
|
988
|
+
if (birth !== null && typeof processGroupId === "number") {
|
|
989
|
+
record = { ...birth, marker, process_group_id: processGroupId };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
trackedHelperProcesses.set(child, record);
|
|
993
|
+
const releaseIfStale = () => {
|
|
994
|
+
setTimeout(() => {
|
|
995
|
+
if (trackedHelperProcesses.get(child) !== record)
|
|
996
|
+
return;
|
|
997
|
+
if (ownerHelperIdentityState(record) !== "stale")
|
|
998
|
+
return;
|
|
999
|
+
activeReaper?.untrackHelper(marker);
|
|
1000
|
+
trackedHelperProcesses.delete(child);
|
|
1001
|
+
}, 0).unref();
|
|
1002
|
+
};
|
|
1003
|
+
child.once("error", releaseIfStale);
|
|
1004
|
+
child.once("exit", releaseIfStale);
|
|
1005
|
+
if (!("state" in record)) {
|
|
1006
|
+
try {
|
|
1007
|
+
reaper?.bindHelper(marker, record);
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
if (terminateOwnerHelperAfterRegistrationFailure(child, record)) {
|
|
1011
|
+
try {
|
|
1012
|
+
reaper?.untrackHelper(marker);
|
|
1013
|
+
trackedHelperProcesses.delete(child);
|
|
1014
|
+
}
|
|
1015
|
+
catch { }
|
|
1016
|
+
}
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return child;
|
|
1022
|
+
}
|
|
1023
|
+
export function signalOwnerTrackedHelper(child, signal) {
|
|
1024
|
+
const identity = trackedHelperProcesses.get(child);
|
|
1025
|
+
if (identity !== undefined)
|
|
1026
|
+
return signalOwnerHelper(identity, signal);
|
|
1027
|
+
try {
|
|
1028
|
+
return child.kill(signal);
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
export function ownerTrackedHelperState(child) {
|
|
1035
|
+
const identity = trackedHelperProcesses.get(child);
|
|
1036
|
+
if (identity !== undefined)
|
|
1037
|
+
return ownerHelperIdentityState(identity);
|
|
1038
|
+
return child.exitCode !== null || child.signalCode !== null ? "stale" : "matching";
|
|
1039
|
+
}
|
|
1040
|
+
export async function waitForOwnerTrackedHelperExit(child, timeoutMs) {
|
|
1041
|
+
const deadline = Date.now() + timeoutMs;
|
|
1042
|
+
while (ownerTrackedHelperState(child) !== "stale" && Date.now() < deadline) {
|
|
1043
|
+
await new Promise((resolveWait) => {
|
|
1044
|
+
const timer = setTimeout(resolveWait, Math.min(25, Math.max(1, deadline - Date.now())));
|
|
1045
|
+
timer.unref();
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return ownerTrackedHelperState(child) === "stale";
|
|
1049
|
+
}
|
|
1050
|
+
export function releaseOwnerTrackedHelper(child) {
|
|
1051
|
+
const record = trackedHelperProcesses.get(child);
|
|
1052
|
+
if (record === undefined || ownerHelperIdentityState(record) !== "stale")
|
|
1053
|
+
return false;
|
|
1054
|
+
activeReaper?.untrackHelper(record.marker);
|
|
1055
|
+
trackedHelperProcesses.delete(child);
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
//# sourceMappingURL=owner-process-reaper.js.map
|