@warlock.js/process 0.0.0-stage → 5.34.0
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +59 -2
- package/cjs/index.cjs +1023 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/guardian.mjs +134 -0
- package/esm/guardian.mjs.map +1 -0
- package/esm/identity.d.mts +7 -0
- package/esm/identity.d.mts.map +1 -0
- package/esm/identity.mjs +86 -0
- package/esm/identity.mjs.map +1 -0
- package/esm/index.d.mts +5 -0
- package/esm/index.mjs +5 -0
- package/esm/platform.d.mts +7 -0
- package/esm/platform.d.mts.map +1 -0
- package/esm/platform.mjs +156 -0
- package/esm/platform.mjs.map +1 -0
- package/esm/registry.mjs +118 -0
- package/esm/registry.mjs.map +1 -0
- package/esm/resources.mjs +85 -0
- package/esm/resources.mjs.map +1 -0
- package/esm/supervisor.d.mts +5 -0
- package/esm/supervisor.d.mts.map +1 -0
- package/esm/supervisor.mjs +443 -0
- package/esm/supervisor.mjs.map +1 -0
- package/esm/types.d.mts +84 -0
- package/esm/types.d.mts.map +1 -0
- package/llms-full.txt +157 -0
- package/llms.txt +9 -0
- package/package.json +49 -4
- package/skills/supervise-processes/SKILL.md +147 -0
package/cjs/index.cjs
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_child_process = require("node:child_process");
|
|
30
|
+
let node_crypto = require("node:crypto");
|
|
31
|
+
let node_path = require("node:path");
|
|
32
|
+
node_path = __toESM(node_path, 1);
|
|
33
|
+
let node_stream = require("node:stream");
|
|
34
|
+
let node_timers_promises = require("node:timers/promises");
|
|
35
|
+
let node_fs_promises = require("node:fs/promises");
|
|
36
|
+
let node_util = require("node:util");
|
|
37
|
+
let node_net = require("node:net");
|
|
38
|
+
|
|
39
|
+
//#region ../../packages/process/src/resources.ts
|
|
40
|
+
function mountedCgroup(membership) {
|
|
41
|
+
const mount = "/sys/fs/cgroup";
|
|
42
|
+
const directory = node_path.default.resolve(mount, `.${membership}`);
|
|
43
|
+
return directory === mount || directory.startsWith(`${mount}/`) ? directory : void 0;
|
|
44
|
+
}
|
|
45
|
+
function validateLimits(limits, platform = process.platform) {
|
|
46
|
+
for (const [key, value] of Object.entries(limits)) if (![
|
|
47
|
+
"timeoutMs",
|
|
48
|
+
"memoryBytes",
|
|
49
|
+
"cpuSeconds",
|
|
50
|
+
"virtualMemoryBytes"
|
|
51
|
+
].includes(key) || !Number.isSafeInteger(value) || value <= 0) throw new Error(`Invalid process limit: ${key}`);
|
|
52
|
+
if (platform !== "linux" && limits.virtualMemoryBytes !== void 0) throw new Error("virtualMemoryBytes is supported only on Linux");
|
|
53
|
+
if (platform === "win32" && limits.cpuSeconds !== void 0) throw new Error("cpuSeconds is supported only on POSIX platforms");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* POSIX shell applies inherited limits. Linux stops here; macOS execs a gated guardian.
|
|
57
|
+
* Flags/units: Apple's bash-3.2/builtins/ulimit.def and installed Bash manual.
|
|
58
|
+
* https://github.com/apple-oss-distributions/bash/blob/main/bash-3.2/builtins/ulimit.def
|
|
59
|
+
*/
|
|
60
|
+
function launcher(limits, platform = process.platform) {
|
|
61
|
+
const commands = ["set -e"];
|
|
62
|
+
if (limits.cpuSeconds) commands.push(`ulimit -t ${limits.cpuSeconds}`);
|
|
63
|
+
if (limits.virtualMemoryBytes) commands.push(`ulimit -v ${Math.ceil(limits.virtualMemoryBytes / 1024)}`);
|
|
64
|
+
if (limits.memoryBytes) commands.push(`ulimit -${platform === "darwin" ? "d" : "m"} ${Math.ceil(limits.memoryBytes / 1024)}`);
|
|
65
|
+
if (platform === "linux") commands.push("kill -STOP \"$$\"");
|
|
66
|
+
commands.push("exec \"$@\"");
|
|
67
|
+
return commands.join("\n");
|
|
68
|
+
}
|
|
69
|
+
/** https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html */
|
|
70
|
+
async function attachCgroup(pid, token, limits, root) {
|
|
71
|
+
if (process.platform !== "linux") return void 0;
|
|
72
|
+
let directory;
|
|
73
|
+
try {
|
|
74
|
+
const own = (await (0, node_fs_promises.readFile)("/proc/self/cgroup", "utf8")).split("\n").find((line) => line.startsWith("0::"))?.slice(3);
|
|
75
|
+
if (!root && own === void 0) return void 0;
|
|
76
|
+
const parent = root ?? mountedCgroup(own ?? "/");
|
|
77
|
+
if (!parent) return void 0;
|
|
78
|
+
await (0, node_fs_promises.readFile)(node_path.default.join(parent, "cgroup.controllers"), "utf8");
|
|
79
|
+
directory = node_path.default.join(parent, `warlock-${token}`);
|
|
80
|
+
await (0, node_fs_promises.mkdir)(directory);
|
|
81
|
+
if (limits.memoryBytes) {
|
|
82
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(directory, "memory.max"), String(limits.memoryBytes));
|
|
83
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(directory, "memory.oom.group"), "1");
|
|
84
|
+
}
|
|
85
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(directory, "cgroup.procs"), String(pid));
|
|
86
|
+
return directory;
|
|
87
|
+
} catch {
|
|
88
|
+
if (directory) await (0, node_fs_promises.rmdir)(directory).catch(() => void 0);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function cgroupMemory(directory) {
|
|
93
|
+
const [peak, events] = await Promise.all([(0, node_fs_promises.readFile)(node_path.default.join(directory, "memory.peak"), "utf8").catch(() => "0"), (0, node_fs_promises.readFile)(node_path.default.join(directory, "memory.events"), "utf8").catch(() => "")]);
|
|
94
|
+
return {
|
|
95
|
+
peak: Number(peak) || 0,
|
|
96
|
+
oom: Number(/^oom_kill (\d+)/m.exec(events)?.[1] ?? 0) > 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Only reattach the token-specific cgroup confirmed by the kernel membership file. */
|
|
100
|
+
async function recoveredCgroup(pid, token) {
|
|
101
|
+
if (process.platform !== "linux") return void 0;
|
|
102
|
+
let source;
|
|
103
|
+
try {
|
|
104
|
+
source = await (0, node_fs_promises.readFile)(`/proc/${pid}/cgroup`, "utf8");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (["ENOENT", "ESRCH"].includes(String(error.code))) return void 0;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const membership = source.split("\n").find((line) => line.startsWith("0::"))?.slice(3);
|
|
110
|
+
if (!membership || node_path.default.basename(membership) !== `warlock-${token}`) return void 0;
|
|
111
|
+
return mountedCgroup(membership);
|
|
112
|
+
}
|
|
113
|
+
async function cleanCgroup(directory) {
|
|
114
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(directory, "cgroup.kill"), "1").catch(() => void 0);
|
|
115
|
+
await (0, node_fs_promises.rmdir)(directory).catch(() => void 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region ../../packages/process/src/guardian.ts
|
|
120
|
+
function guardianAddress(platform, pid, uid) {
|
|
121
|
+
return platform === "win32" ? `\\\\.\\pipe\\warlock-process-${pid}` : `/tmp/warlock-process-${uid}-${pid}.sock`;
|
|
122
|
+
}
|
|
123
|
+
/** A live guardian reads its own environment, never argv, to prove ownership.
|
|
124
|
+
* Node IPC is native-free: https://nodejs.org/api/net.html#ipc-support
|
|
125
|
+
* The stable root PID stays alive for the workload and survives supervisor restarts.
|
|
126
|
+
*/
|
|
127
|
+
const guardianScript = String.raw`
|
|
128
|
+
const { createServer } = require("node:net");
|
|
129
|
+
const { createHmac } = require("node:crypto");
|
|
130
|
+
const { spawn } = require("node:child_process");
|
|
131
|
+
const { chmodSync, unlinkSync } = require("node:fs");
|
|
132
|
+
const token = process.env.WARLOCK_PROCESS_TOKEN;
|
|
133
|
+
const address = process.platform === "win32"
|
|
134
|
+
? "\\\\.\\pipe\\warlock-process-" + process.pid
|
|
135
|
+
: "/tmp/warlock-process-" + process.getuid() + "-" + process.pid + ".sock";
|
|
136
|
+
let gated = true;
|
|
137
|
+
const finish = (code) => { server.close(); process.exit(code); };
|
|
138
|
+
const start = () => {
|
|
139
|
+
if (!gated) return;
|
|
140
|
+
gated = false;
|
|
141
|
+
const [command, ...args] = JSON.parse(process.env.WARLOCK_PROCESS_LAUNCH);
|
|
142
|
+
const env = { ...process.env };
|
|
143
|
+
for (const key of Object.keys(env)) {
|
|
144
|
+
const name = process.platform === "win32" ? key.toUpperCase() : key;
|
|
145
|
+
if (name === "WARLOCK_PROCESS_TOKEN" || name === "WARLOCK_PROCESS_LAUNCH") delete env[key];
|
|
146
|
+
}
|
|
147
|
+
const child = spawn(command, args, { env, stdio: "inherit", windowsHide: true });
|
|
148
|
+
child.on("error", (error) => { console.error(error.message); finish(127); });
|
|
149
|
+
child.on("exit", (code, signal) => {
|
|
150
|
+
if (signal && process.platform !== "win32") process.kill(process.pid, signal);
|
|
151
|
+
else finish(code ?? 1);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const server = createServer((socket) => {
|
|
155
|
+
socket.setTimeout(2000, () => socket.destroy());
|
|
156
|
+
socket.on("error", () => {});
|
|
157
|
+
let input = "";
|
|
158
|
+
socket.on("data", (chunk) => {
|
|
159
|
+
input += chunk;
|
|
160
|
+
if (input.length > 1024) { socket.destroy(); return; }
|
|
161
|
+
if (!input.endsWith("\n")) return;
|
|
162
|
+
try {
|
|
163
|
+
const request = JSON.parse(input);
|
|
164
|
+
if (!/^[a-f0-9]{64}$/.test(request.nonce)) { socket.destroy(); return; }
|
|
165
|
+
const sign = (value) => createHmac("sha256", token).update(value).digest("hex");
|
|
166
|
+
if (request.resume === sign(request.nonce + ":resume")) start();
|
|
167
|
+
socket.end(JSON.stringify({ pid: process.pid, digest: sign(request.nonce + ":" + process.pid + ":" + gated), gated }) + "\n");
|
|
168
|
+
} catch { socket.destroy(); }
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
server.on("error", (error) => { console.error(error.message); process.exit(126); });
|
|
172
|
+
if (process.platform !== "win32") {
|
|
173
|
+
try { unlinkSync(address); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
174
|
+
process.on("exit", () => { try { unlinkSync(address); } catch {} });
|
|
175
|
+
}
|
|
176
|
+
server.listen(address, () => { if (process.platform !== "win32") chmodSync(address, 0o600); });
|
|
177
|
+
`;
|
|
178
|
+
async function guardianProof(pid, resume, expectedToken = resume) {
|
|
179
|
+
if (!expectedToken) return void 0;
|
|
180
|
+
const token = expectedToken;
|
|
181
|
+
const nonce = (0, node_crypto.randomBytes)(32).toString("hex");
|
|
182
|
+
const sign = (value) => (0, node_crypto.createHmac)("sha256", token).update(value).digest("hex");
|
|
183
|
+
const address = guardianAddress(process.platform, pid, process.getuid?.() ?? 0);
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const socket = (0, node_net.connect)(address);
|
|
186
|
+
let data = "";
|
|
187
|
+
const fail = (error) => {
|
|
188
|
+
socket.destroy();
|
|
189
|
+
reject(error);
|
|
190
|
+
};
|
|
191
|
+
socket.setTimeout(2e3, () => fail(/* @__PURE__ */ new Error("Process ownership observation timed out")));
|
|
192
|
+
socket.on("connect", () => socket.write(JSON.stringify({
|
|
193
|
+
nonce,
|
|
194
|
+
resume: resume ? sign(nonce + ":resume") : void 0
|
|
195
|
+
}) + "\n"));
|
|
196
|
+
socket.on("data", (chunk) => {
|
|
197
|
+
data += chunk.toString();
|
|
198
|
+
if (data.length > 1024) fail(/* @__PURE__ */ new Error("Invalid process ownership response"));
|
|
199
|
+
});
|
|
200
|
+
socket.on("error", (error) => {
|
|
201
|
+
if (["ENOENT", "ECONNREFUSED"].includes(error.code ?? "")) resolve(void 0);
|
|
202
|
+
else reject(error);
|
|
203
|
+
});
|
|
204
|
+
socket.on("end", () => {
|
|
205
|
+
try {
|
|
206
|
+
resolve(parseGuardianProof(data, pid, nonce, token));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
reject(error);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Malformed observation is an error; an authenticated identity mismatch is unowned. */
|
|
214
|
+
function parseGuardianProof(source, pid, nonce, token) {
|
|
215
|
+
const proof = JSON.parse(source);
|
|
216
|
+
if (!proof || typeof proof !== "object" || !("pid" in proof) || typeof proof.pid !== "number" || !("gated" in proof) || typeof proof.gated !== "boolean" || !("digest" in proof) || typeof proof.digest !== "string") throw new Error("Invalid process ownership response");
|
|
217
|
+
const expected = (0, node_crypto.createHmac)("sha256", token).update(nonce + ":" + pid + ":" + proof.gated).digest("hex");
|
|
218
|
+
if (proof.pid !== pid || proof.digest !== expected) return void 0;
|
|
219
|
+
return {
|
|
220
|
+
pid,
|
|
221
|
+
token,
|
|
222
|
+
gated: proof.gated
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** Pure launch construction, including case-insensitive Windows environment keys. */
|
|
226
|
+
function launchCommand(platform, node, command, args, limits, environment, token) {
|
|
227
|
+
const portable = platform !== "linux";
|
|
228
|
+
const env = Object.fromEntries(Object.entries(environment).filter(([key]) => !["WARLOCK_PROCESS_TOKEN", "WARLOCK_PROCESS_LAUNCH"].includes(platform === "win32" ? key.toUpperCase() : key)));
|
|
229
|
+
env.WARLOCK_PROCESS_TOKEN = token;
|
|
230
|
+
if (portable) env.WARLOCK_PROCESS_LAUNCH = JSON.stringify([command, ...args]);
|
|
231
|
+
const executable = portable ? node : command;
|
|
232
|
+
const arguments_ = portable ? ["-e", guardianScript] : args;
|
|
233
|
+
return {
|
|
234
|
+
file: platform === "win32" ? executable : "/bin/sh",
|
|
235
|
+
args: platform === "win32" ? arguments_ : [
|
|
236
|
+
"-c",
|
|
237
|
+
launcher(limits, platform),
|
|
238
|
+
"warlock-process",
|
|
239
|
+
executable,
|
|
240
|
+
...arguments_
|
|
241
|
+
],
|
|
242
|
+
env
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region ../../packages/process/src/platform.ts
|
|
248
|
+
var UnsupportedProcessPlatformError = class extends Error {
|
|
249
|
+
platform;
|
|
250
|
+
constructor(platform) {
|
|
251
|
+
super(`Process supervision is unsupported on ${platform}`);
|
|
252
|
+
this.platform = platform;
|
|
253
|
+
this.name = "UnsupportedProcessPlatformError";
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function supported(platform) {
|
|
257
|
+
if (![
|
|
258
|
+
"linux",
|
|
259
|
+
"darwin",
|
|
260
|
+
"win32"
|
|
261
|
+
].includes(platform)) throw new UnsupportedProcessPlatformError(platform);
|
|
262
|
+
}
|
|
263
|
+
function validPid(pid) {
|
|
264
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) throw new Error("Invalid process PID");
|
|
265
|
+
}
|
|
266
|
+
/** Apple ps(1): lstart uses strftime %c. LC_ALL=C gives %a %b %e %H:%M:%S %Y.
|
|
267
|
+
* https://github.com/apple-oss-distributions/adv_cmds/blob/main/ps/ps.1
|
|
268
|
+
* https://github.com/apple-oss-distributions/adv_cmds/blob/main/ps/print.c (lstarted)
|
|
269
|
+
* https://github.com/apple-oss-distributions/Libc/blob/main/stdtime/FreeBSD/timelocal.c (_C_time_locale)
|
|
270
|
+
*/
|
|
271
|
+
function parseDarwinStart(source) {
|
|
272
|
+
const match = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/.exec(source.trim());
|
|
273
|
+
if (!match) throw new Error("Invalid macOS process start time");
|
|
274
|
+
const [, weekday, month, day, hour, minute, second, year] = match;
|
|
275
|
+
const monthIndex = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ").indexOf(month ?? "");
|
|
276
|
+
const date = new Date(Date.UTC(Number(year), monthIndex, Number(day), Number(hour), Number(minute), Number(second)));
|
|
277
|
+
if (date.getUTCFullYear() !== Number(year) || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== Number(day) || date.getUTCHours() !== Number(hour) || date.getUTCMinutes() !== Number(minute) || date.getUTCSeconds() !== Number(second) || "Sun Mon Tue Wed Thu Fri Sat".split(" ")[date.getUTCDay()] !== weekday) throw new Error("Invalid macOS process start time");
|
|
278
|
+
return String(date.getTime() / 1e3);
|
|
279
|
+
}
|
|
280
|
+
function darwinCommand(pid, tree = false) {
|
|
281
|
+
validPid(pid);
|
|
282
|
+
return {
|
|
283
|
+
file: "/bin/ps",
|
|
284
|
+
args: [
|
|
285
|
+
...tree ? ["-ax"] : ["-p", String(pid)],
|
|
286
|
+
"-o",
|
|
287
|
+
"pid=,ppid=,pgid=,state=,rss=,lstart="
|
|
288
|
+
],
|
|
289
|
+
env: {
|
|
290
|
+
LC_ALL: "C",
|
|
291
|
+
TZ: "UTC"
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function parseDarwinRows(source) {
|
|
296
|
+
return source.trim() ? source.trim().split("\n").map((line) => {
|
|
297
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
|
|
298
|
+
if (!match) throw new Error("Invalid macOS process observation");
|
|
299
|
+
return {
|
|
300
|
+
pid: Number(match[1]),
|
|
301
|
+
parent: Number(match[2]),
|
|
302
|
+
group: Number(match[3]),
|
|
303
|
+
state: match[4] ?? "",
|
|
304
|
+
rss: Number(match[5]) * 1024,
|
|
305
|
+
start: parseDarwinStart(match[6] ?? "")
|
|
306
|
+
};
|
|
307
|
+
}) : [];
|
|
308
|
+
}
|
|
309
|
+
/** CIM CreationDate is a DateTime; serialize UTC ticks as text, never a locale date
|
|
310
|
+
* or JSON number (100ns ticks exceed Number's precision). WorkingSetSize is bytes.
|
|
311
|
+
* https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-process
|
|
312
|
+
*/
|
|
313
|
+
function windowsCommand(pid, tree = false) {
|
|
314
|
+
validPid(pid);
|
|
315
|
+
return {
|
|
316
|
+
file: "powershell.exe",
|
|
317
|
+
args: [
|
|
318
|
+
"-NoLogo",
|
|
319
|
+
"-NoProfile",
|
|
320
|
+
"-NonInteractive",
|
|
321
|
+
"-Command",
|
|
322
|
+
[
|
|
323
|
+
"$ErrorActionPreference = 'Stop'",
|
|
324
|
+
`$rows = @(${`Get-CimInstance Win32_Process${tree ? "" : ` -Filter 'ProcessId=${pid}'`}`} | ForEach-Object {`,
|
|
325
|
+
`if ([int]$_.ProcessId -eq ${pid} -and $null -eq $_.CreationDate) { throw 'Cannot read process creation time' }`,
|
|
326
|
+
"if ($null -ne $_.CreationDate) {",
|
|
327
|
+
"[pscustomobject]@{ pid = [int]$_.ProcessId; parent = [int]$_.ParentProcessId; start = $_.CreationDate.ToUniversalTime().Ticks.ToString([Globalization.CultureInfo]::InvariantCulture); rss = [double]$_.WorkingSetSize }",
|
|
328
|
+
"}",
|
|
329
|
+
"})",
|
|
330
|
+
"ConvertTo-Json -InputObject $rows -Compress"
|
|
331
|
+
].join("\n")
|
|
332
|
+
]
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function parseWindowsRows(source) {
|
|
336
|
+
const value = JSON.parse(source.replace(/^\uFEFF/, ""));
|
|
337
|
+
if (!Array.isArray(value)) throw new Error("Invalid Windows process observation");
|
|
338
|
+
return value.map((row) => {
|
|
339
|
+
if (!row || typeof row !== "object" || !("pid" in row) || typeof row.pid !== "number" || !Number.isSafeInteger(row.pid) || row.pid < 0 || !("parent" in row) || typeof row.parent !== "number" || !Number.isSafeInteger(row.parent) || row.parent < 0 || !("start" in row) || typeof row.start !== "string" || !/^\d{18}$/.test(row.start) || !("rss" in row) || typeof row.rss !== "number" || !Number.isFinite(row.rss) || row.rss < 0) throw new Error("Invalid Windows process observation");
|
|
340
|
+
return {
|
|
341
|
+
pid: row.pid,
|
|
342
|
+
parent: row.parent,
|
|
343
|
+
start: row.start,
|
|
344
|
+
rss: row.rss,
|
|
345
|
+
group: row.pid,
|
|
346
|
+
state: "R"
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function taskkillCommand(pid) {
|
|
351
|
+
validPid(pid);
|
|
352
|
+
return {
|
|
353
|
+
file: "taskkill.exe",
|
|
354
|
+
args: [
|
|
355
|
+
"/PID",
|
|
356
|
+
String(pid),
|
|
357
|
+
"/T",
|
|
358
|
+
"/F"
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function treeMemory(rows, pid) {
|
|
363
|
+
const root = rows.find((row) => row.pid === pid);
|
|
364
|
+
if (!root) return 0;
|
|
365
|
+
const included = /* @__PURE__ */ new Map([[pid, root]]);
|
|
366
|
+
let changed = true;
|
|
367
|
+
while (changed) {
|
|
368
|
+
changed = false;
|
|
369
|
+
for (const row of rows) {
|
|
370
|
+
const parent = included.get(row.parent);
|
|
371
|
+
if (!included.has(row.pid) && parent && BigInt(row.start) >= BigInt(parent.start)) {
|
|
372
|
+
included.set(row.pid, row);
|
|
373
|
+
changed = true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return [...included.values()].reduce((total, row) => total + row.rss, 0);
|
|
378
|
+
}
|
|
379
|
+
/** All OS effects are injected; foreign-platform specs never execute real commands. */
|
|
380
|
+
async function portableIdentity(platform, pid, token, execute, readProof) {
|
|
381
|
+
validPid(pid);
|
|
382
|
+
const command = platform === "darwin" ? darwinCommand : windowsCommand;
|
|
383
|
+
const parse = platform === "darwin" ? parseDarwinRows : parseWindowsRows;
|
|
384
|
+
const rows = parse(await execute(command(pid, token)));
|
|
385
|
+
const before = rows.find((row) => row.pid === pid);
|
|
386
|
+
if (!before || /^[ZX]/.test(before.state)) return void 0;
|
|
387
|
+
if (!token) return before;
|
|
388
|
+
const proof = await readProof(pid);
|
|
389
|
+
const after = parse(await execute(command(pid))).find((row) => row.pid === pid);
|
|
390
|
+
if (!after || after.start !== before.start || after.group !== before.group || /^[ZX]/.test(after.state)) return void 0;
|
|
391
|
+
return {
|
|
392
|
+
start: after.start,
|
|
393
|
+
group: after.group,
|
|
394
|
+
state: proof?.pid === pid && proof.gated ? "T" : after.state,
|
|
395
|
+
rss: treeMemory(rows, pid),
|
|
396
|
+
token: proof?.pid === pid ? proof.token : void 0
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region ../../packages/process/src/identity.ts
|
|
402
|
+
const execute = (0, node_util.promisify)(node_child_process.execFile);
|
|
403
|
+
async function executeCommand(command) {
|
|
404
|
+
const { stdout } = await execute(command.file, command.args, {
|
|
405
|
+
env: {
|
|
406
|
+
...process.env,
|
|
407
|
+
...command.env
|
|
408
|
+
},
|
|
409
|
+
timeout: 5e3,
|
|
410
|
+
maxBuffer: 4194304,
|
|
411
|
+
windowsHide: true
|
|
412
|
+
});
|
|
413
|
+
return stdout;
|
|
414
|
+
}
|
|
415
|
+
const TOKEN_ENV = "WARLOCK_PROCESS_TOKEN";
|
|
416
|
+
function statFields(stat) {
|
|
417
|
+
return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* A task that is exiting has already released its memory map (stat field 23,
|
|
421
|
+
* vsize, reads 0) before it turns into a zombie. Linux then reports its /proc
|
|
422
|
+
* files as root-owned, so it is never a live child to verify or signal.
|
|
423
|
+
*/
|
|
424
|
+
function exiting(fields) {
|
|
425
|
+
return ["Z", "X"].includes(fields[0] ?? "") || fields[20] === "0";
|
|
426
|
+
}
|
|
427
|
+
/** /proc fields: https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html */
|
|
428
|
+
async function identity(pid, token = false, expectedToken) {
|
|
429
|
+
if (!Number.isSafeInteger(pid) || pid <= 1) return void 0;
|
|
430
|
+
try {
|
|
431
|
+
if (process.platform === "linux") {
|
|
432
|
+
const fields = statFields(await (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"));
|
|
433
|
+
const state = fields[0];
|
|
434
|
+
const start = fields[19];
|
|
435
|
+
if (!state || !start || exiting(fields)) return void 0;
|
|
436
|
+
const boot = (await (0, node_fs_promises.readFile)("/proc/sys/kernel/random/boot_id", "utf8")).trim();
|
|
437
|
+
const status = await (0, node_fs_promises.readFile)(`/proc/${pid}/status`, "utf8");
|
|
438
|
+
let environment = "";
|
|
439
|
+
if (token) try {
|
|
440
|
+
environment = await (0, node_fs_promises.readFile)(`/proc/${pid}/environ`, "utf8");
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error.code !== "EACCES") throw error;
|
|
443
|
+
const now = statFields(await (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"));
|
|
444
|
+
if (now[19] !== start || exiting(now)) return void 0;
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
const after = statFields(await (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"));
|
|
448
|
+
if (after[19] !== start || after[2] !== fields[2] || exiting(after)) return void 0;
|
|
449
|
+
return {
|
|
450
|
+
start: `${boot}:${start}`,
|
|
451
|
+
group: Number(fields[2]),
|
|
452
|
+
state: after[0] ?? state,
|
|
453
|
+
rss: Number(/^VmRSS:\s+(\d+)/m.exec(status)?.[1] ?? 0) * 1024,
|
|
454
|
+
token: environment.split("\0").find((entry) => entry.startsWith(`${TOKEN_ENV}=`))?.slice(22)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (process.platform === "darwin" || process.platform === "win32") return await portableIdentity(process.platform, pid, token, executeCommand, (selected) => guardianProof(selected, void 0, expectedToken));
|
|
458
|
+
return;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
const code = error.code;
|
|
461
|
+
if (process.platform === "linux" && ["ENOENT", "ESRCH"].includes(String(code)) || process.platform === "darwin" && code === 1 && !error.stdout?.trim()) return void 0;
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
var ProcessNotOwnedError = class extends Error {
|
|
466
|
+
pid;
|
|
467
|
+
constructor(pid) {
|
|
468
|
+
super(`Process ${pid} does not have a verified supervisor identity`);
|
|
469
|
+
this.pid = pid;
|
|
470
|
+
this.name = "ProcessNotOwnedError";
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
async function owns(entry) {
|
|
474
|
+
const current = await identity(entry.pid, true, entry.token);
|
|
475
|
+
return current?.start === entry.start && current.token === entry.token && current.group === entry.pid;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../../packages/process/src/registry.ts
|
|
480
|
+
function record(value) {
|
|
481
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
482
|
+
}
|
|
483
|
+
function strings(value) {
|
|
484
|
+
return record(value) && Object.values(value).every((item) => typeof item === "string");
|
|
485
|
+
}
|
|
486
|
+
function parseRegistry(source, owner) {
|
|
487
|
+
const value = JSON.parse(source);
|
|
488
|
+
if (!record(value) || value.version !== 1 || value.owner !== owner || !Array.isArray(value.entries)) throw new Error("Invalid process registry or different owner");
|
|
489
|
+
const entries = [];
|
|
490
|
+
for (const entry of value.entries) {
|
|
491
|
+
if (!record(entry) || entry.owner !== owner || typeof entry.pid !== "number" || !Number.isSafeInteger(entry.pid) || Number(entry.pid) <= 1 || typeof entry.token !== "string" || !/^[a-f0-9]{64}$/.test(entry.token) || typeof entry.start !== "string" || typeof entry.type !== "string" || typeof entry.command !== "string" || !Array.isArray(entry.args) || !entry.args.every((arg) => typeof arg === "string") || !strings(entry.labels) || typeof entry.startedAt !== "number" || !Number.isFinite(entry.startedAt) || entry.startedAt < 0 || !record(entry.limits) || !Object.entries(entry.limits).every(([key, limit]) => [
|
|
492
|
+
"timeoutMs",
|
|
493
|
+
"memoryBytes",
|
|
494
|
+
"cpuSeconds",
|
|
495
|
+
"virtualMemoryBytes"
|
|
496
|
+
].includes(key) && typeof limit === "number" && Number.isSafeInteger(limit) && limit > 0) || ![
|
|
497
|
+
"cgroup-v2",
|
|
498
|
+
"rlimit-polling",
|
|
499
|
+
"rss-polling"
|
|
500
|
+
].includes(String(entry.enforcement)) || typeof entry.peakMemoryBytes !== "number" || !Number.isFinite(entry.peakMemoryBytes) || entry.peakMemoryBytes < 0 || entry.cgroup !== void 0 && typeof entry.cgroup !== "string" || entry.gated !== void 0 && typeof entry.gated !== "boolean") throw new Error("Invalid process registry entry");
|
|
501
|
+
entries.push(entry);
|
|
502
|
+
}
|
|
503
|
+
if (new Set(entries.map((entry) => entry.pid)).size !== entries.length) throw new Error("Duplicate registry PID");
|
|
504
|
+
return entries;
|
|
505
|
+
}
|
|
506
|
+
/** One writer per registry. A crash leaves a lease which only a dead identity can release. */
|
|
507
|
+
var Registry = class {
|
|
508
|
+
file;
|
|
509
|
+
owner;
|
|
510
|
+
queue = Promise.resolve();
|
|
511
|
+
locked = false;
|
|
512
|
+
constructor(file, owner) {
|
|
513
|
+
this.file = file;
|
|
514
|
+
this.owner = owner;
|
|
515
|
+
}
|
|
516
|
+
async open() {
|
|
517
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(this.file), {
|
|
518
|
+
recursive: true,
|
|
519
|
+
mode: 448
|
|
520
|
+
});
|
|
521
|
+
const self = await identity(process.pid);
|
|
522
|
+
if (!self) throw new Error("Cannot verify registry writer identity");
|
|
523
|
+
const lock = `${this.file}.lock`;
|
|
524
|
+
try {
|
|
525
|
+
await (0, node_fs_promises.mkdir)(lock, { mode: 448 });
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (error.code !== "EEXIST") throw error;
|
|
528
|
+
const reclaim = `${lock}.reclaim`;
|
|
529
|
+
await (0, node_fs_promises.mkdir)(reclaim, { mode: 448 });
|
|
530
|
+
try {
|
|
531
|
+
const lease = JSON.parse(await (0, node_fs_promises.readFile)(node_path.default.join(lock, "writer.json"), "utf8"));
|
|
532
|
+
if (!record(lease) || typeof lease.pid !== "number" || typeof lease.start !== "string") throw new Error("Invalid registry writer lease; inspect it manually");
|
|
533
|
+
if ((await identity(lease.pid))?.start === lease.start) throw new Error("Process registry is already in use");
|
|
534
|
+
const stale = `${lock}.${(0, node_crypto.randomBytes)(8).toString("hex")}`;
|
|
535
|
+
await (0, node_fs_promises.rename)(lock, stale);
|
|
536
|
+
await (0, node_fs_promises.rm)(stale, { recursive: true });
|
|
537
|
+
await (0, node_fs_promises.mkdir)(lock, { mode: 448 });
|
|
538
|
+
} finally {
|
|
539
|
+
await (0, node_fs_promises.rm)(reclaim, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.locked = true;
|
|
543
|
+
try {
|
|
544
|
+
await (0, node_fs_promises.writeFile)(node_path.default.join(lock, "writer.json"), JSON.stringify({
|
|
545
|
+
pid: process.pid,
|
|
546
|
+
start: self.start
|
|
547
|
+
}), { mode: 384 });
|
|
548
|
+
try {
|
|
549
|
+
return parseRegistry(await (0, node_fs_promises.readFile)(this.file, "utf8"), this.owner);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error.code === "ENOENT") return [];
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
await this.close();
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
save(entries) {
|
|
560
|
+
const source = JSON.stringify({
|
|
561
|
+
version: 1,
|
|
562
|
+
owner: this.owner,
|
|
563
|
+
entries
|
|
564
|
+
}, null, 2) + "\n";
|
|
565
|
+
const next = this.queue.then(async () => {
|
|
566
|
+
const temporary = `${this.file}.${(0, node_crypto.randomBytes)(8).toString("hex")}.tmp`;
|
|
567
|
+
try {
|
|
568
|
+
await (0, node_fs_promises.writeFile)(temporary, source, {
|
|
569
|
+
mode: 384,
|
|
570
|
+
flag: "wx"
|
|
571
|
+
});
|
|
572
|
+
await (0, node_fs_promises.rename)(temporary, this.file);
|
|
573
|
+
} finally {
|
|
574
|
+
await (0, node_fs_promises.rm)(temporary, { force: true });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
this.queue = next.catch(() => void 0);
|
|
578
|
+
return next;
|
|
579
|
+
}
|
|
580
|
+
async close() {
|
|
581
|
+
await this.queue;
|
|
582
|
+
if (this.locked) {
|
|
583
|
+
await (0, node_fs_promises.rm)(`${this.file}.lock`, { recursive: true });
|
|
584
|
+
this.locked = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region ../../packages/process/src/supervisor.ts
|
|
591
|
+
function info(entry) {
|
|
592
|
+
const { token: _token, start: _start, cgroup: _cgroup, gated: _gated, ...visible } = entry;
|
|
593
|
+
return structuredClone(visible);
|
|
594
|
+
}
|
|
595
|
+
function validateFilter(filter) {
|
|
596
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter) || Object.keys(filter).some((key) => key !== "labels")) throw new Error("Only label filters are supported");
|
|
597
|
+
if (filter.labels !== void 0 && (filter.labels === null || typeof filter.labels !== "object" || Array.isArray(filter.labels) || !Object.values(filter.labels).every((value) => typeof value === "string"))) throw new Error("Labels must be string key/value pairs");
|
|
598
|
+
}
|
|
599
|
+
function matches(entry, filter) {
|
|
600
|
+
return Object.entries(filter.labels ?? {}).every(([key, value]) => entry.labels[key] === value);
|
|
601
|
+
}
|
|
602
|
+
function supervisor(options) {
|
|
603
|
+
for (const option of ["owner", "registry"]) if (!options || typeof options[option] !== "string" || !options[option].trim()) throw new TypeError(`Invalid supervisor configuration: ${option} must be a nonempty string`);
|
|
604
|
+
validateLimits(options.defaults ?? {});
|
|
605
|
+
options = {
|
|
606
|
+
...options,
|
|
607
|
+
defaults: { ...options.defaults }
|
|
608
|
+
};
|
|
609
|
+
const pollMs = options.pollIntervalMs ?? 100;
|
|
610
|
+
const graceMs = options.stopGraceMs ?? 1e3;
|
|
611
|
+
if (!Number.isSafeInteger(pollMs) || pollMs < 10 || !Number.isSafeInteger(graceMs) || graceMs < 0) throw new Error("Invalid polling interval or stop grace period");
|
|
612
|
+
const registry = new Registry(node_path.default.resolve(options.registry), options.owner);
|
|
613
|
+
const entries = /* @__PURE__ */ new Map();
|
|
614
|
+
const active = /* @__PURE__ */ new Map();
|
|
615
|
+
let ready;
|
|
616
|
+
let closing = false;
|
|
617
|
+
let shutdown;
|
|
618
|
+
let queue = Promise.resolve();
|
|
619
|
+
function serial(operation) {
|
|
620
|
+
const next = queue.then(operation);
|
|
621
|
+
queue = next.catch(() => void 0);
|
|
622
|
+
return next;
|
|
623
|
+
}
|
|
624
|
+
async function open() {
|
|
625
|
+
supported(process.platform);
|
|
626
|
+
if (closing) throw new Error("Supervisor has shut down");
|
|
627
|
+
ready ??= registry.open().then((saved) => {
|
|
628
|
+
for (const entry of saved) {
|
|
629
|
+
delete entry.cgroup;
|
|
630
|
+
entries.set(entry.pid, entry);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
await ready;
|
|
634
|
+
}
|
|
635
|
+
const save = () => registry.save([...entries.values()]);
|
|
636
|
+
async function signal(entry, value) {
|
|
637
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
638
|
+
if (process.platform !== "linux" && value === "SIGCONT") {
|
|
639
|
+
const proof = await guardianProof(entry.pid, entry.token);
|
|
640
|
+
if (!proof || proof.token !== entry.token || proof.gated) throw new ProcessNotOwnedError(entry.pid);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (process.platform === "win32") {
|
|
644
|
+
try {
|
|
645
|
+
await executeCommand(taskkillCommand(entry.pid));
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (await owns(entry)) throw error;
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
process.kill(-entry.pid, value);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (error.code !== "ESRCH") throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function stopEntry(entry, reason = "stopped") {
|
|
658
|
+
const tracked = active.get(entry.pid);
|
|
659
|
+
let running;
|
|
660
|
+
if (!tracked) {
|
|
661
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
662
|
+
entry.cgroup = await recoveredCgroup(entry.pid, entry.token);
|
|
663
|
+
running = track(entry);
|
|
664
|
+
} else running = tracked;
|
|
665
|
+
if (running.finished) return running.finish(null, null);
|
|
666
|
+
if (running.stopping) return running.stopping;
|
|
667
|
+
const stopping = (async () => {
|
|
668
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
669
|
+
running.reason ??= reason;
|
|
670
|
+
await signal(entry, "SIGTERM");
|
|
671
|
+
const deadline = Date.now() + graceMs;
|
|
672
|
+
while (await owns(entry)) {
|
|
673
|
+
if (Date.now() >= deadline) {
|
|
674
|
+
await signal(entry, "SIGKILL");
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
await (0, node_timers_promises.setTimeout)(20);
|
|
678
|
+
}
|
|
679
|
+
const killedDeadline = Date.now() + 5e3;
|
|
680
|
+
while (await owns(entry)) {
|
|
681
|
+
if (Date.now() >= killedDeadline) throw new Error(`Process ${entry.pid} did not exit after SIGKILL`);
|
|
682
|
+
await (0, node_timers_promises.setTimeout)(20);
|
|
683
|
+
}
|
|
684
|
+
await running.finish(null, null);
|
|
685
|
+
})();
|
|
686
|
+
running.stopping = stopping;
|
|
687
|
+
try {
|
|
688
|
+
await stopping;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
running.stopping = void 0;
|
|
691
|
+
if (error instanceof ProcessNotOwnedError && !await identity(entry.pid)) {
|
|
692
|
+
await running.finish(null, null);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function track(entry, streams) {
|
|
699
|
+
let resolveExit = () => void 0;
|
|
700
|
+
let rejectExit = () => void 0;
|
|
701
|
+
const exited = new Promise((resolve, reject) => {
|
|
702
|
+
resolveExit = resolve;
|
|
703
|
+
rejectExit = reject;
|
|
704
|
+
});
|
|
705
|
+
exited.catch(() => void 0);
|
|
706
|
+
const stdout = streams?.stdout ?? new node_stream.PassThrough();
|
|
707
|
+
const stderr = streams?.stderr ?? new node_stream.PassThrough();
|
|
708
|
+
const stdin = streams?.stdin ?? new node_stream.PassThrough();
|
|
709
|
+
let finishing;
|
|
710
|
+
const running = {
|
|
711
|
+
entry,
|
|
712
|
+
finished: false,
|
|
713
|
+
adopted: !streams,
|
|
714
|
+
fail: (error) => rejectExit(error),
|
|
715
|
+
handle: {
|
|
716
|
+
pid: entry.pid,
|
|
717
|
+
stdout,
|
|
718
|
+
stderr,
|
|
719
|
+
stdin,
|
|
720
|
+
exited,
|
|
721
|
+
stop: () => serial(async () => {
|
|
722
|
+
if (!running.finished) await stopEntry(entry);
|
|
723
|
+
})
|
|
724
|
+
},
|
|
725
|
+
finish(code, sig) {
|
|
726
|
+
finishing ??= (async () => {
|
|
727
|
+
running.finished = true;
|
|
728
|
+
clearInterval(running.timer);
|
|
729
|
+
if (entry.cgroup) {
|
|
730
|
+
const memory = await cgroupMemory(entry.cgroup);
|
|
731
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);
|
|
732
|
+
if (memory.oom) running.reason = "memory-cap";
|
|
733
|
+
await cleanCgroup(entry.cgroup);
|
|
734
|
+
}
|
|
735
|
+
active.delete(entry.pid);
|
|
736
|
+
entries.delete(entry.pid);
|
|
737
|
+
try {
|
|
738
|
+
await save();
|
|
739
|
+
resolveExit({
|
|
740
|
+
reason: running.reason ?? "exit",
|
|
741
|
+
code,
|
|
742
|
+
signal: sig,
|
|
743
|
+
peakMemoryBytes: entry.peakMemoryBytes
|
|
744
|
+
});
|
|
745
|
+
} catch (error) {
|
|
746
|
+
rejectExit(error);
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
if (!streams) {
|
|
750
|
+
stdout.end();
|
|
751
|
+
stderr.end();
|
|
752
|
+
stdin.end();
|
|
753
|
+
}
|
|
754
|
+
})();
|
|
755
|
+
return finishing;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
active.set(entry.pid, running);
|
|
759
|
+
return running;
|
|
760
|
+
}
|
|
761
|
+
function monitor(running) {
|
|
762
|
+
if (running.finished) return;
|
|
763
|
+
let sampling = false;
|
|
764
|
+
const sample = async () => {
|
|
765
|
+
if (sampling || running.finished) return;
|
|
766
|
+
sampling = true;
|
|
767
|
+
try {
|
|
768
|
+
const entry = running.entry;
|
|
769
|
+
const current = await identity(entry.pid, true, entry.token);
|
|
770
|
+
if (running.finished) return;
|
|
771
|
+
if (current?.start !== entry.start || current.token !== entry.token || current.group !== entry.pid) {
|
|
772
|
+
if (!current) {
|
|
773
|
+
if (running.adopted) await running.finish(null, null);
|
|
774
|
+
} else throw new ProcessNotOwnedError(entry.pid);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, current.rss);
|
|
778
|
+
if (entry.cgroup) {
|
|
779
|
+
const memory = await cgroupMemory(entry.cgroup);
|
|
780
|
+
if (running.finished) return;
|
|
781
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);
|
|
782
|
+
if (memory.oom) running.reason = "memory-cap";
|
|
783
|
+
}
|
|
784
|
+
if (entry.limits.memoryBytes && entry.peakMemoryBytes > entry.limits.memoryBytes) await stopEntry(entry, "memory-cap");
|
|
785
|
+
else if (entry.limits.timeoutMs && Date.now() - entry.startedAt >= entry.limits.timeoutMs) await stopEntry(entry, "timeout");
|
|
786
|
+
} finally {
|
|
787
|
+
sampling = false;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
running.timer = setInterval(() => {
|
|
791
|
+
sample().catch((error) => {
|
|
792
|
+
clearInterval(running.timer);
|
|
793
|
+
if (!running.finished) running.fail(error);
|
|
794
|
+
});
|
|
795
|
+
}, pollMs);
|
|
796
|
+
running.timer.unref();
|
|
797
|
+
}
|
|
798
|
+
async function launch(input, terminal) {
|
|
799
|
+
await open();
|
|
800
|
+
if ([...entries.keys()].some((pid) => !active.has(pid))) throw new Error("Call recover() before spawning with an existing registry");
|
|
801
|
+
if (typeof input.command !== "string" || !input.command || typeof input.kind !== "string" || !input.kind) throw new Error("kind and command are required");
|
|
802
|
+
validateFilter({ labels: input.labels });
|
|
803
|
+
if (terminal && [input.columns ?? 80, input.rows ?? 24].some((size) => !Number.isSafeInteger(size) || size <= 0)) throw new Error("Terminal dimensions must be positive safe integers");
|
|
804
|
+
const limits = {
|
|
805
|
+
...options.defaults,
|
|
806
|
+
...input.limits
|
|
807
|
+
};
|
|
808
|
+
validateLimits(limits);
|
|
809
|
+
const token = (0, node_crypto.randomBytes)(32).toString("hex");
|
|
810
|
+
const { file: executable, args, env } = launchCommand(process.platform, process.execPath, input.command, input.args ?? [], limits, {
|
|
811
|
+
...process.env,
|
|
812
|
+
...input.env
|
|
813
|
+
}, token);
|
|
814
|
+
let pid;
|
|
815
|
+
let streams;
|
|
816
|
+
let terminalProcess;
|
|
817
|
+
let exit;
|
|
818
|
+
let spawnError;
|
|
819
|
+
let running;
|
|
820
|
+
const onExit = (code, sig) => {
|
|
821
|
+
exit = {
|
|
822
|
+
code,
|
|
823
|
+
signal: sig
|
|
824
|
+
};
|
|
825
|
+
if (running) running.finish(code, sig).catch(() => void 0);
|
|
826
|
+
};
|
|
827
|
+
if (terminal) {
|
|
828
|
+
terminalProcess = (await import("node-pty")).spawn(executable, args, {
|
|
829
|
+
cwd: input.cwd,
|
|
830
|
+
env: Object.fromEntries(Object.entries(env).filter((pair) => pair[1] !== void 0)),
|
|
831
|
+
cols: input.columns ?? 80,
|
|
832
|
+
rows: input.rows ?? 24,
|
|
833
|
+
name: "xterm-256color"
|
|
834
|
+
});
|
|
835
|
+
pid = terminalProcess.pid;
|
|
836
|
+
const output = new node_stream.PassThrough();
|
|
837
|
+
const errors = new node_stream.PassThrough();
|
|
838
|
+
const instance = terminalProcess;
|
|
839
|
+
const dataSubscription = instance.onData((data) => output.write(data));
|
|
840
|
+
const inputStream = new node_stream.Writable({ write(chunk, _encoding, callback) {
|
|
841
|
+
instance.write(chunk.toString());
|
|
842
|
+
callback();
|
|
843
|
+
} });
|
|
844
|
+
const exitSubscription = instance.onExit((event) => {
|
|
845
|
+
dataSubscription.dispose();
|
|
846
|
+
exitSubscription.dispose();
|
|
847
|
+
output.end();
|
|
848
|
+
errors.end();
|
|
849
|
+
inputStream.end();
|
|
850
|
+
onExit(event.exitCode, event.signal ? String(event.signal) : null);
|
|
851
|
+
});
|
|
852
|
+
streams = {
|
|
853
|
+
stdout: output,
|
|
854
|
+
stderr: errors,
|
|
855
|
+
stdin: inputStream
|
|
856
|
+
};
|
|
857
|
+
} else {
|
|
858
|
+
const child = (0, node_child_process.spawn)(executable, args, {
|
|
859
|
+
cwd: input.cwd,
|
|
860
|
+
env,
|
|
861
|
+
detached: process.platform !== "win32",
|
|
862
|
+
windowsHide: true,
|
|
863
|
+
stdio: "pipe"
|
|
864
|
+
});
|
|
865
|
+
child.on("error", (error) => {
|
|
866
|
+
spawnError = error;
|
|
867
|
+
});
|
|
868
|
+
child.on("exit", onExit);
|
|
869
|
+
if (!child.pid) throw new Error("Could not start process");
|
|
870
|
+
pid = child.pid;
|
|
871
|
+
streams = {
|
|
872
|
+
stdout: child.stdout,
|
|
873
|
+
stderr: child.stderr,
|
|
874
|
+
stdin: child.stdin
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
let entry;
|
|
878
|
+
try {
|
|
879
|
+
const deadline = Date.now() + 5e3;
|
|
880
|
+
let current = await identity(pid, true, token);
|
|
881
|
+
while (!current?.state.startsWith("T") || current.token !== token) {
|
|
882
|
+
if (spawnError) throw spawnError;
|
|
883
|
+
if (exit || Date.now() >= deadline) throw new Error("Child failed to reach the resource setup gate");
|
|
884
|
+
await (0, node_timers_promises.setTimeout)(10);
|
|
885
|
+
current = await identity(pid, true, token);
|
|
886
|
+
}
|
|
887
|
+
if (current.group !== pid) throw new Error("Child did not create an isolated process group");
|
|
888
|
+
entry = {
|
|
889
|
+
pid,
|
|
890
|
+
owner: options.owner,
|
|
891
|
+
type: input.kind,
|
|
892
|
+
command: input.command,
|
|
893
|
+
args: [...input.args ?? []],
|
|
894
|
+
labels: { ...input.labels },
|
|
895
|
+
startedAt: Date.now(),
|
|
896
|
+
limits,
|
|
897
|
+
enforcement: process.platform === "win32" ? "rss-polling" : "rlimit-polling",
|
|
898
|
+
peakMemoryBytes: current.rss,
|
|
899
|
+
token,
|
|
900
|
+
start: current.start,
|
|
901
|
+
gated: true
|
|
902
|
+
};
|
|
903
|
+
entry.cgroup = await attachCgroup(pid, token, limits, options.cgroupRoot);
|
|
904
|
+
if (entry.cgroup) entry.enforcement = "cgroup-v2";
|
|
905
|
+
entries.set(pid, entry);
|
|
906
|
+
await save();
|
|
907
|
+
running = track(entry, streams);
|
|
908
|
+
await signal(entry, "SIGCONT");
|
|
909
|
+
delete entry.gated;
|
|
910
|
+
await save();
|
|
911
|
+
monitor(running);
|
|
912
|
+
if (terminalProcess) {
|
|
913
|
+
const instance = terminalProcess;
|
|
914
|
+
return {
|
|
915
|
+
...running.handle,
|
|
916
|
+
write: (data) => instance.write(data),
|
|
917
|
+
resize: (columns, rows) => instance.resize(columns, rows)
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return running.handle;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
const current = await identity(pid, true, token);
|
|
923
|
+
if (current?.token === token) {
|
|
924
|
+
const failed = {
|
|
925
|
+
pid,
|
|
926
|
+
token,
|
|
927
|
+
start: current.start
|
|
928
|
+
};
|
|
929
|
+
if (await owns(failed)) try {
|
|
930
|
+
if (process.platform === "win32") await executeCommand(taskkillCommand(pid));
|
|
931
|
+
else process.kill(-pid, "SIGKILL");
|
|
932
|
+
} catch {}
|
|
933
|
+
}
|
|
934
|
+
if (entry && !running) {
|
|
935
|
+
entries.delete(pid);
|
|
936
|
+
await save();
|
|
937
|
+
}
|
|
938
|
+
if (entry?.cgroup) await cleanCgroup(entry.cgroup);
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
spawn: (input) => serial(() => launch(input, false)),
|
|
944
|
+
spawnPty: (input) => serial(async () => await launch(input, true)),
|
|
945
|
+
list: (filter = {}) => serial(async () => {
|
|
946
|
+
validateFilter(filter);
|
|
947
|
+
await open();
|
|
948
|
+
return [...entries.values()].filter((entry) => matches(entry, filter)).map(info);
|
|
949
|
+
}),
|
|
950
|
+
stop: (filter = {}) => serial(async () => {
|
|
951
|
+
validateFilter(filter);
|
|
952
|
+
await open();
|
|
953
|
+
for (const entry of [...entries.values()].filter((entry) => matches(entry, filter))) await stopEntry(entry);
|
|
954
|
+
}),
|
|
955
|
+
stopPid: (pid) => serial(async () => {
|
|
956
|
+
await open();
|
|
957
|
+
const entry = entries.get(pid);
|
|
958
|
+
if (!entry) throw new ProcessNotOwnedError(pid);
|
|
959
|
+
await stopEntry(entry);
|
|
960
|
+
}),
|
|
961
|
+
recover: (recovery = {}) => serial(async () => {
|
|
962
|
+
await open();
|
|
963
|
+
const result = {
|
|
964
|
+
adopted: [],
|
|
965
|
+
stopped: [],
|
|
966
|
+
cleared: []
|
|
967
|
+
};
|
|
968
|
+
for (const entry of [...entries.values()]) {
|
|
969
|
+
if (active.has(entry.pid)) continue;
|
|
970
|
+
if (!await owns(entry)) {
|
|
971
|
+
entries.delete(entry.pid);
|
|
972
|
+
result.cleared.push(entry.pid);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
entry.cgroup = await recoveredCgroup(entry.pid, entry.token);
|
|
976
|
+
entry.enforcement = entry.cgroup ? "cgroup-v2" : process.platform === "win32" ? "rss-polling" : "rlimit-polling";
|
|
977
|
+
if (await recovery.keep?.(info(entry))) {
|
|
978
|
+
monitor(track(entry));
|
|
979
|
+
if (entry.gated) {
|
|
980
|
+
await signal(entry, "SIGCONT");
|
|
981
|
+
delete entry.gated;
|
|
982
|
+
}
|
|
983
|
+
result.adopted.push(entry.pid);
|
|
984
|
+
} else {
|
|
985
|
+
await stopEntry(entry);
|
|
986
|
+
result.stopped.push(entry.pid);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
await save();
|
|
990
|
+
return result;
|
|
991
|
+
}),
|
|
992
|
+
stopAll() {
|
|
993
|
+
shutdown ??= serial(async () => {
|
|
994
|
+
await open();
|
|
995
|
+
closing = true;
|
|
996
|
+
try {
|
|
997
|
+
const errors = [];
|
|
998
|
+
for (const entry of [...entries.values()]) try {
|
|
999
|
+
if (await owns(entry)) await stopEntry(entry);
|
|
1000
|
+
else {
|
|
1001
|
+
const running = active.get(entry.pid);
|
|
1002
|
+
if (running) await running.finish(null, null);
|
|
1003
|
+
else entries.delete(entry.pid);
|
|
1004
|
+
}
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
errors.push(error);
|
|
1007
|
+
}
|
|
1008
|
+
await save();
|
|
1009
|
+
if (errors.length) throw new AggregateError(errors, "Some owned processes could not be stopped");
|
|
1010
|
+
} finally {
|
|
1011
|
+
await registry.close();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
return shutdown;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
//#endregion
|
|
1020
|
+
exports.ProcessNotOwnedError = ProcessNotOwnedError;
|
|
1021
|
+
exports.UnsupportedProcessPlatformError = UnsupportedProcessPlatformError;
|
|
1022
|
+
exports.supervisor = supervisor;
|
|
1023
|
+
//# sourceMappingURL=index.cjs.map
|