@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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { attachCgroup, cgroupMemory, cleanCgroup, recoveredCgroup, validateLimits } from "./resources.mjs";
|
|
2
|
+
import { guardianProof, launchCommand } from "./guardian.mjs";
|
|
3
|
+
import { supported, taskkillCommand } from "./platform.mjs";
|
|
4
|
+
import { ProcessNotOwnedError, executeCommand, identity, owns } from "./identity.mjs";
|
|
5
|
+
import { Registry } from "./registry.mjs";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { PassThrough, Writable } from "node:stream";
|
|
10
|
+
import { setTimeout } from "node:timers/promises";
|
|
11
|
+
|
|
12
|
+
//#region ../../packages/process/src/supervisor.ts
|
|
13
|
+
function info(entry) {
|
|
14
|
+
const { token: _token, start: _start, cgroup: _cgroup, gated: _gated, ...visible } = entry;
|
|
15
|
+
return structuredClone(visible);
|
|
16
|
+
}
|
|
17
|
+
function validateFilter(filter) {
|
|
18
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter) || Object.keys(filter).some((key) => key !== "labels")) throw new Error("Only label filters are supported");
|
|
19
|
+
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");
|
|
20
|
+
}
|
|
21
|
+
function matches(entry, filter) {
|
|
22
|
+
return Object.entries(filter.labels ?? {}).every(([key, value]) => entry.labels[key] === value);
|
|
23
|
+
}
|
|
24
|
+
function supervisor(options) {
|
|
25
|
+
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`);
|
|
26
|
+
validateLimits(options.defaults ?? {});
|
|
27
|
+
options = {
|
|
28
|
+
...options,
|
|
29
|
+
defaults: { ...options.defaults }
|
|
30
|
+
};
|
|
31
|
+
const pollMs = options.pollIntervalMs ?? 100;
|
|
32
|
+
const graceMs = options.stopGraceMs ?? 1e3;
|
|
33
|
+
if (!Number.isSafeInteger(pollMs) || pollMs < 10 || !Number.isSafeInteger(graceMs) || graceMs < 0) throw new Error("Invalid polling interval or stop grace period");
|
|
34
|
+
const registry = new Registry(path.resolve(options.registry), options.owner);
|
|
35
|
+
const entries = /* @__PURE__ */ new Map();
|
|
36
|
+
const active = /* @__PURE__ */ new Map();
|
|
37
|
+
let ready;
|
|
38
|
+
let closing = false;
|
|
39
|
+
let shutdown;
|
|
40
|
+
let queue = Promise.resolve();
|
|
41
|
+
function serial(operation) {
|
|
42
|
+
const next = queue.then(operation);
|
|
43
|
+
queue = next.catch(() => void 0);
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
async function open() {
|
|
47
|
+
supported(process.platform);
|
|
48
|
+
if (closing) throw new Error("Supervisor has shut down");
|
|
49
|
+
ready ??= registry.open().then((saved) => {
|
|
50
|
+
for (const entry of saved) {
|
|
51
|
+
delete entry.cgroup;
|
|
52
|
+
entries.set(entry.pid, entry);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await ready;
|
|
56
|
+
}
|
|
57
|
+
const save = () => registry.save([...entries.values()]);
|
|
58
|
+
async function signal(entry, value) {
|
|
59
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
60
|
+
if (process.platform !== "linux" && value === "SIGCONT") {
|
|
61
|
+
const proof = await guardianProof(entry.pid, entry.token);
|
|
62
|
+
if (!proof || proof.token !== entry.token || proof.gated) throw new ProcessNotOwnedError(entry.pid);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
try {
|
|
67
|
+
await executeCommand(taskkillCommand(entry.pid));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (await owns(entry)) throw error;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
process.kill(-entry.pid, value);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code !== "ESRCH") throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function stopEntry(entry, reason = "stopped") {
|
|
80
|
+
const tracked = active.get(entry.pid);
|
|
81
|
+
let running;
|
|
82
|
+
if (!tracked) {
|
|
83
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
84
|
+
entry.cgroup = await recoveredCgroup(entry.pid, entry.token);
|
|
85
|
+
running = track(entry);
|
|
86
|
+
} else running = tracked;
|
|
87
|
+
if (running.finished) return running.finish(null, null);
|
|
88
|
+
if (running.stopping) return running.stopping;
|
|
89
|
+
const stopping = (async () => {
|
|
90
|
+
if (!await owns(entry)) throw new ProcessNotOwnedError(entry.pid);
|
|
91
|
+
running.reason ??= reason;
|
|
92
|
+
await signal(entry, "SIGTERM");
|
|
93
|
+
const deadline = Date.now() + graceMs;
|
|
94
|
+
while (await owns(entry)) {
|
|
95
|
+
if (Date.now() >= deadline) {
|
|
96
|
+
await signal(entry, "SIGKILL");
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
await setTimeout(20);
|
|
100
|
+
}
|
|
101
|
+
const killedDeadline = Date.now() + 5e3;
|
|
102
|
+
while (await owns(entry)) {
|
|
103
|
+
if (Date.now() >= killedDeadline) throw new Error(`Process ${entry.pid} did not exit after SIGKILL`);
|
|
104
|
+
await setTimeout(20);
|
|
105
|
+
}
|
|
106
|
+
await running.finish(null, null);
|
|
107
|
+
})();
|
|
108
|
+
running.stopping = stopping;
|
|
109
|
+
try {
|
|
110
|
+
await stopping;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
running.stopping = void 0;
|
|
113
|
+
if (error instanceof ProcessNotOwnedError && !await identity(entry.pid)) {
|
|
114
|
+
await running.finish(null, null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function track(entry, streams) {
|
|
121
|
+
let resolveExit = () => void 0;
|
|
122
|
+
let rejectExit = () => void 0;
|
|
123
|
+
const exited = new Promise((resolve, reject) => {
|
|
124
|
+
resolveExit = resolve;
|
|
125
|
+
rejectExit = reject;
|
|
126
|
+
});
|
|
127
|
+
exited.catch(() => void 0);
|
|
128
|
+
const stdout = streams?.stdout ?? new PassThrough();
|
|
129
|
+
const stderr = streams?.stderr ?? new PassThrough();
|
|
130
|
+
const stdin = streams?.stdin ?? new PassThrough();
|
|
131
|
+
let finishing;
|
|
132
|
+
const running = {
|
|
133
|
+
entry,
|
|
134
|
+
finished: false,
|
|
135
|
+
adopted: !streams,
|
|
136
|
+
fail: (error) => rejectExit(error),
|
|
137
|
+
handle: {
|
|
138
|
+
pid: entry.pid,
|
|
139
|
+
stdout,
|
|
140
|
+
stderr,
|
|
141
|
+
stdin,
|
|
142
|
+
exited,
|
|
143
|
+
stop: () => serial(async () => {
|
|
144
|
+
if (!running.finished) await stopEntry(entry);
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
finish(code, sig) {
|
|
148
|
+
finishing ??= (async () => {
|
|
149
|
+
running.finished = true;
|
|
150
|
+
clearInterval(running.timer);
|
|
151
|
+
if (entry.cgroup) {
|
|
152
|
+
const memory = await cgroupMemory(entry.cgroup);
|
|
153
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);
|
|
154
|
+
if (memory.oom) running.reason = "memory-cap";
|
|
155
|
+
await cleanCgroup(entry.cgroup);
|
|
156
|
+
}
|
|
157
|
+
active.delete(entry.pid);
|
|
158
|
+
entries.delete(entry.pid);
|
|
159
|
+
try {
|
|
160
|
+
await save();
|
|
161
|
+
resolveExit({
|
|
162
|
+
reason: running.reason ?? "exit",
|
|
163
|
+
code,
|
|
164
|
+
signal: sig,
|
|
165
|
+
peakMemoryBytes: entry.peakMemoryBytes
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
rejectExit(error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
if (!streams) {
|
|
172
|
+
stdout.end();
|
|
173
|
+
stderr.end();
|
|
174
|
+
stdin.end();
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
return finishing;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
active.set(entry.pid, running);
|
|
181
|
+
return running;
|
|
182
|
+
}
|
|
183
|
+
function monitor(running) {
|
|
184
|
+
if (running.finished) return;
|
|
185
|
+
let sampling = false;
|
|
186
|
+
const sample = async () => {
|
|
187
|
+
if (sampling || running.finished) return;
|
|
188
|
+
sampling = true;
|
|
189
|
+
try {
|
|
190
|
+
const entry = running.entry;
|
|
191
|
+
const current = await identity(entry.pid, true, entry.token);
|
|
192
|
+
if (running.finished) return;
|
|
193
|
+
if (current?.start !== entry.start || current.token !== entry.token || current.group !== entry.pid) {
|
|
194
|
+
if (!current) {
|
|
195
|
+
if (running.adopted) await running.finish(null, null);
|
|
196
|
+
} else throw new ProcessNotOwnedError(entry.pid);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, current.rss);
|
|
200
|
+
if (entry.cgroup) {
|
|
201
|
+
const memory = await cgroupMemory(entry.cgroup);
|
|
202
|
+
if (running.finished) return;
|
|
203
|
+
entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);
|
|
204
|
+
if (memory.oom) running.reason = "memory-cap";
|
|
205
|
+
}
|
|
206
|
+
if (entry.limits.memoryBytes && entry.peakMemoryBytes > entry.limits.memoryBytes) await stopEntry(entry, "memory-cap");
|
|
207
|
+
else if (entry.limits.timeoutMs && Date.now() - entry.startedAt >= entry.limits.timeoutMs) await stopEntry(entry, "timeout");
|
|
208
|
+
} finally {
|
|
209
|
+
sampling = false;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
running.timer = setInterval(() => {
|
|
213
|
+
sample().catch((error) => {
|
|
214
|
+
clearInterval(running.timer);
|
|
215
|
+
if (!running.finished) running.fail(error);
|
|
216
|
+
});
|
|
217
|
+
}, pollMs);
|
|
218
|
+
running.timer.unref();
|
|
219
|
+
}
|
|
220
|
+
async function launch(input, terminal) {
|
|
221
|
+
await open();
|
|
222
|
+
if ([...entries.keys()].some((pid) => !active.has(pid))) throw new Error("Call recover() before spawning with an existing registry");
|
|
223
|
+
if (typeof input.command !== "string" || !input.command || typeof input.kind !== "string" || !input.kind) throw new Error("kind and command are required");
|
|
224
|
+
validateFilter({ labels: input.labels });
|
|
225
|
+
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");
|
|
226
|
+
const limits = {
|
|
227
|
+
...options.defaults,
|
|
228
|
+
...input.limits
|
|
229
|
+
};
|
|
230
|
+
validateLimits(limits);
|
|
231
|
+
const token = randomBytes(32).toString("hex");
|
|
232
|
+
const { file: executable, args, env } = launchCommand(process.platform, process.execPath, input.command, input.args ?? [], limits, {
|
|
233
|
+
...process.env,
|
|
234
|
+
...input.env
|
|
235
|
+
}, token);
|
|
236
|
+
let pid;
|
|
237
|
+
let streams;
|
|
238
|
+
let terminalProcess;
|
|
239
|
+
let exit;
|
|
240
|
+
let spawnError;
|
|
241
|
+
let running;
|
|
242
|
+
const onExit = (code, sig) => {
|
|
243
|
+
exit = {
|
|
244
|
+
code,
|
|
245
|
+
signal: sig
|
|
246
|
+
};
|
|
247
|
+
if (running) running.finish(code, sig).catch(() => void 0);
|
|
248
|
+
};
|
|
249
|
+
if (terminal) {
|
|
250
|
+
terminalProcess = (await import("node-pty")).spawn(executable, args, {
|
|
251
|
+
cwd: input.cwd,
|
|
252
|
+
env: Object.fromEntries(Object.entries(env).filter((pair) => pair[1] !== void 0)),
|
|
253
|
+
cols: input.columns ?? 80,
|
|
254
|
+
rows: input.rows ?? 24,
|
|
255
|
+
name: "xterm-256color"
|
|
256
|
+
});
|
|
257
|
+
pid = terminalProcess.pid;
|
|
258
|
+
const output = new PassThrough();
|
|
259
|
+
const errors = new PassThrough();
|
|
260
|
+
const instance = terminalProcess;
|
|
261
|
+
const dataSubscription = instance.onData((data) => output.write(data));
|
|
262
|
+
const inputStream = new Writable({ write(chunk, _encoding, callback) {
|
|
263
|
+
instance.write(chunk.toString());
|
|
264
|
+
callback();
|
|
265
|
+
} });
|
|
266
|
+
const exitSubscription = instance.onExit((event) => {
|
|
267
|
+
dataSubscription.dispose();
|
|
268
|
+
exitSubscription.dispose();
|
|
269
|
+
output.end();
|
|
270
|
+
errors.end();
|
|
271
|
+
inputStream.end();
|
|
272
|
+
onExit(event.exitCode, event.signal ? String(event.signal) : null);
|
|
273
|
+
});
|
|
274
|
+
streams = {
|
|
275
|
+
stdout: output,
|
|
276
|
+
stderr: errors,
|
|
277
|
+
stdin: inputStream
|
|
278
|
+
};
|
|
279
|
+
} else {
|
|
280
|
+
const child = spawn(executable, args, {
|
|
281
|
+
cwd: input.cwd,
|
|
282
|
+
env,
|
|
283
|
+
detached: process.platform !== "win32",
|
|
284
|
+
windowsHide: true,
|
|
285
|
+
stdio: "pipe"
|
|
286
|
+
});
|
|
287
|
+
child.on("error", (error) => {
|
|
288
|
+
spawnError = error;
|
|
289
|
+
});
|
|
290
|
+
child.on("exit", onExit);
|
|
291
|
+
if (!child.pid) throw new Error("Could not start process");
|
|
292
|
+
pid = child.pid;
|
|
293
|
+
streams = {
|
|
294
|
+
stdout: child.stdout,
|
|
295
|
+
stderr: child.stderr,
|
|
296
|
+
stdin: child.stdin
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let entry;
|
|
300
|
+
try {
|
|
301
|
+
const deadline = Date.now() + 5e3;
|
|
302
|
+
let current = await identity(pid, true, token);
|
|
303
|
+
while (!current?.state.startsWith("T") || current.token !== token) {
|
|
304
|
+
if (spawnError) throw spawnError;
|
|
305
|
+
if (exit || Date.now() >= deadline) throw new Error("Child failed to reach the resource setup gate");
|
|
306
|
+
await setTimeout(10);
|
|
307
|
+
current = await identity(pid, true, token);
|
|
308
|
+
}
|
|
309
|
+
if (current.group !== pid) throw new Error("Child did not create an isolated process group");
|
|
310
|
+
entry = {
|
|
311
|
+
pid,
|
|
312
|
+
owner: options.owner,
|
|
313
|
+
type: input.kind,
|
|
314
|
+
command: input.command,
|
|
315
|
+
args: [...input.args ?? []],
|
|
316
|
+
labels: { ...input.labels },
|
|
317
|
+
startedAt: Date.now(),
|
|
318
|
+
limits,
|
|
319
|
+
enforcement: process.platform === "win32" ? "rss-polling" : "rlimit-polling",
|
|
320
|
+
peakMemoryBytes: current.rss,
|
|
321
|
+
token,
|
|
322
|
+
start: current.start,
|
|
323
|
+
gated: true
|
|
324
|
+
};
|
|
325
|
+
entry.cgroup = await attachCgroup(pid, token, limits, options.cgroupRoot);
|
|
326
|
+
if (entry.cgroup) entry.enforcement = "cgroup-v2";
|
|
327
|
+
entries.set(pid, entry);
|
|
328
|
+
await save();
|
|
329
|
+
running = track(entry, streams);
|
|
330
|
+
await signal(entry, "SIGCONT");
|
|
331
|
+
delete entry.gated;
|
|
332
|
+
await save();
|
|
333
|
+
monitor(running);
|
|
334
|
+
if (terminalProcess) {
|
|
335
|
+
const instance = terminalProcess;
|
|
336
|
+
return {
|
|
337
|
+
...running.handle,
|
|
338
|
+
write: (data) => instance.write(data),
|
|
339
|
+
resize: (columns, rows) => instance.resize(columns, rows)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return running.handle;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const current = await identity(pid, true, token);
|
|
345
|
+
if (current?.token === token) {
|
|
346
|
+
const failed = {
|
|
347
|
+
pid,
|
|
348
|
+
token,
|
|
349
|
+
start: current.start
|
|
350
|
+
};
|
|
351
|
+
if (await owns(failed)) try {
|
|
352
|
+
if (process.platform === "win32") await executeCommand(taskkillCommand(pid));
|
|
353
|
+
else process.kill(-pid, "SIGKILL");
|
|
354
|
+
} catch {}
|
|
355
|
+
}
|
|
356
|
+
if (entry && !running) {
|
|
357
|
+
entries.delete(pid);
|
|
358
|
+
await save();
|
|
359
|
+
}
|
|
360
|
+
if (entry?.cgroup) await cleanCgroup(entry.cgroup);
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
spawn: (input) => serial(() => launch(input, false)),
|
|
366
|
+
spawnPty: (input) => serial(async () => await launch(input, true)),
|
|
367
|
+
list: (filter = {}) => serial(async () => {
|
|
368
|
+
validateFilter(filter);
|
|
369
|
+
await open();
|
|
370
|
+
return [...entries.values()].filter((entry) => matches(entry, filter)).map(info);
|
|
371
|
+
}),
|
|
372
|
+
stop: (filter = {}) => serial(async () => {
|
|
373
|
+
validateFilter(filter);
|
|
374
|
+
await open();
|
|
375
|
+
for (const entry of [...entries.values()].filter((entry) => matches(entry, filter))) await stopEntry(entry);
|
|
376
|
+
}),
|
|
377
|
+
stopPid: (pid) => serial(async () => {
|
|
378
|
+
await open();
|
|
379
|
+
const entry = entries.get(pid);
|
|
380
|
+
if (!entry) throw new ProcessNotOwnedError(pid);
|
|
381
|
+
await stopEntry(entry);
|
|
382
|
+
}),
|
|
383
|
+
recover: (recovery = {}) => serial(async () => {
|
|
384
|
+
await open();
|
|
385
|
+
const result = {
|
|
386
|
+
adopted: [],
|
|
387
|
+
stopped: [],
|
|
388
|
+
cleared: []
|
|
389
|
+
};
|
|
390
|
+
for (const entry of [...entries.values()]) {
|
|
391
|
+
if (active.has(entry.pid)) continue;
|
|
392
|
+
if (!await owns(entry)) {
|
|
393
|
+
entries.delete(entry.pid);
|
|
394
|
+
result.cleared.push(entry.pid);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
entry.cgroup = await recoveredCgroup(entry.pid, entry.token);
|
|
398
|
+
entry.enforcement = entry.cgroup ? "cgroup-v2" : process.platform === "win32" ? "rss-polling" : "rlimit-polling";
|
|
399
|
+
if (await recovery.keep?.(info(entry))) {
|
|
400
|
+
monitor(track(entry));
|
|
401
|
+
if (entry.gated) {
|
|
402
|
+
await signal(entry, "SIGCONT");
|
|
403
|
+
delete entry.gated;
|
|
404
|
+
}
|
|
405
|
+
result.adopted.push(entry.pid);
|
|
406
|
+
} else {
|
|
407
|
+
await stopEntry(entry);
|
|
408
|
+
result.stopped.push(entry.pid);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await save();
|
|
412
|
+
return result;
|
|
413
|
+
}),
|
|
414
|
+
stopAll() {
|
|
415
|
+
shutdown ??= serial(async () => {
|
|
416
|
+
await open();
|
|
417
|
+
closing = true;
|
|
418
|
+
try {
|
|
419
|
+
const errors = [];
|
|
420
|
+
for (const entry of [...entries.values()]) try {
|
|
421
|
+
if (await owns(entry)) await stopEntry(entry);
|
|
422
|
+
else {
|
|
423
|
+
const running = active.get(entry.pid);
|
|
424
|
+
if (running) await running.finish(null, null);
|
|
425
|
+
else entries.delete(entry.pid);
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
errors.push(error);
|
|
429
|
+
}
|
|
430
|
+
await save();
|
|
431
|
+
if (errors.length) throw new AggregateError(errors, "Some owned processes could not be stopped");
|
|
432
|
+
} finally {
|
|
433
|
+
await registry.close();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return shutdown;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
//#endregion
|
|
442
|
+
export { supervisor };
|
|
443
|
+
//# sourceMappingURL=supervisor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.mjs","names":["delay","spawnChild"],"sources":["../../../../../../../packages/process/src/supervisor.ts"],"sourcesContent":["import { spawn as spawnChild } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport path from \"node:path\";\nimport { PassThrough, Writable } from \"node:stream\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { identity, owns, ProcessNotOwnedError, executeCommand } from \"./identity\";\nimport { guardianProof, launchCommand } from \"./guardian\";\nimport { supported, taskkillCommand } from \"./platform\";\nimport { Registry, type RegistryEntry } from \"./registry\";\nimport {\n attachCgroup,\n cgroupMemory,\n cleanCgroup,\n recoveredCgroup,\n validateLimits,\n} from \"./resources\";\nimport type {\n ProcessExit,\n ProcessFilter,\n ProcessHandle,\n ProcessInfo,\n ProcessSupervisor,\n PtyHandle,\n RecoveryResult,\n SpawnOptions,\n SupervisorOptions,\n} from \"./types\";\n\ntype Pty = {\n pid: number;\n onData(listener: (data: string) => void): { dispose(): void };\n onExit(listener: (event: { exitCode: number; signal?: number }) => void): { dispose(): void };\n write(data: string): void;\n resize(columns: number, rows: number): void;\n};\ntype PtyModule = {\n spawn(\n command: string,\n args: string[],\n options: {\n cwd?: string;\n env: Record<string, string>;\n cols: number;\n rows: number;\n name: string;\n },\n ): Pty;\n};\ntype Active = {\n entry: RegistryEntry;\n handle: ProcessHandle;\n reason?: ProcessExit[\"reason\"];\n finish(code: number | null, signal: string | null): Promise<void>;\n stopping?: Promise<void>;\n timer?: ReturnType<typeof setInterval>;\n finished: boolean;\n adopted: boolean;\n fail(error: unknown): void;\n};\n\nfunction info(entry: RegistryEntry): ProcessInfo {\n const { token: _token, start: _start, cgroup: _cgroup, gated: _gated, ...visible } = entry;\n return structuredClone(visible);\n}\nfunction validateFilter(filter: ProcessFilter): void {\n if (\n !filter ||\n typeof filter !== \"object\" ||\n Array.isArray(filter) ||\n Object.keys(filter).some((key) => key !== \"labels\")\n )\n throw new Error(\"Only label filters are supported\");\n if (\n filter.labels !== undefined &&\n (filter.labels === null ||\n typeof filter.labels !== \"object\" ||\n Array.isArray(filter.labels) ||\n !Object.values(filter.labels).every((value) => typeof value === \"string\"))\n )\n throw new Error(\"Labels must be string key/value pairs\");\n}\nfunction matches(entry: RegistryEntry, filter: ProcessFilter): boolean {\n return Object.entries(filter.labels ?? {}).every(([key, value]) => entry.labels[key] === value);\n}\n\nexport function supervisor(options: SupervisorOptions): ProcessSupervisor {\n for (const option of [\"owner\", \"registry\"] as const) {\n if (!options || typeof options[option] !== \"string\" || !options[option].trim()) {\n throw new TypeError(`Invalid supervisor configuration: ${option} must be a nonempty string`);\n }\n }\n validateLimits(options.defaults ?? {});\n // Caller-owned configuration must not change an active supervisor's identity.\n options = { ...options, defaults: { ...options.defaults } };\n const pollMs = options.pollIntervalMs ?? 100;\n const graceMs = options.stopGraceMs ?? 1000;\n if (!Number.isSafeInteger(pollMs) || pollMs < 10 || !Number.isSafeInteger(graceMs) || graceMs < 0)\n throw new Error(\"Invalid polling interval or stop grace period\");\n const registry = new Registry(path.resolve(options.registry), options.owner);\n const entries = new Map<number, RegistryEntry>();\n const active = new Map<number, Active>();\n let ready: Promise<void> | undefined;\n let closing = false;\n let shutdown: Promise<void> | undefined;\n // Serialize public operations, including shutdown, so a gated spawn cannot escape stopAll.\n let queue: Promise<unknown> = Promise.resolve();\n function serial<T>(operation: () => Promise<T>): Promise<T> {\n const next = queue.then(operation);\n queue = next.catch(() => undefined);\n return next;\n }\n async function open(): Promise<void> {\n supported(process.platform);\n if (closing) throw new Error(\"Supervisor has shut down\");\n ready ??= registry.open().then((saved) => {\n for (const entry of saved) {\n delete entry.cgroup;\n entries.set(entry.pid, entry);\n }\n });\n await ready;\n }\n const save = () => registry.save([...entries.values()]);\n\n async function signal(entry: RegistryEntry, value: NodeJS.Signals): Promise<void> {\n if (!(await owns(entry))) throw new ProcessNotOwnedError(entry.pid);\n if (process.platform !== \"linux\" && value === \"SIGCONT\") {\n const proof = await guardianProof(entry.pid, entry.token);\n if (!proof || proof.token !== entry.token || proof.gated)\n throw new ProcessNotOwnedError(entry.pid);\n return;\n }\n if (process.platform === \"win32\") {\n try {\n await executeCommand(taskkillCommand(entry.pid));\n } catch (error) {\n if (await owns(entry)) throw error;\n }\n return;\n }\n // Negative PID targets only the session/group established for this child.\n // Recheck for every signal, including escalation; never match a command line.\n try {\n process.kill(-entry.pid, value);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ESRCH\") throw error;\n }\n }\n async function stopEntry(\n entry: RegistryEntry,\n reason: ProcessExit[\"reason\"] = \"stopped\",\n ): Promise<void> {\n const tracked = active.get(entry.pid);\n let running: Active;\n if (!tracked) {\n if (!(await owns(entry))) throw new ProcessNotOwnedError(entry.pid);\n entry.cgroup = await recoveredCgroup(entry.pid, entry.token);\n running = track(entry);\n } else running = tracked;\n if (running.finished) return running.finish(null, null);\n if (running.stopping) return running.stopping;\n const stopping = (async () => {\n if (!(await owns(entry))) throw new ProcessNotOwnedError(entry.pid);\n running.reason ??= reason;\n await signal(entry, \"SIGTERM\");\n const deadline = Date.now() + graceMs;\n while (await owns(entry)) {\n if (Date.now() >= deadline) {\n await signal(entry, \"SIGKILL\");\n break;\n }\n await delay(20);\n }\n const killedDeadline = Date.now() + 5000;\n while (await owns(entry)) {\n if (Date.now() >= killedDeadline)\n throw new Error(`Process ${entry.pid} did not exit after SIGKILL`);\n await delay(20);\n }\n await running.finish(null, null);\n })();\n running.stopping = stopping;\n try {\n await stopping;\n } catch (error) {\n running.stopping = undefined;\n // The leader may exit between identity verification and a signal. A\n // missing identity is harmless for a tracked handle; a live mismatch is\n // still an ownership failure and must never receive another signal.\n if (error instanceof ProcessNotOwnedError && !(await identity(entry.pid))) {\n await running.finish(null, null);\n return;\n }\n throw error;\n }\n }\n\n function track(\n entry: RegistryEntry,\n streams?: Pick<ProcessHandle, \"stdin\" | \"stdout\" | \"stderr\">,\n ): Active {\n let resolveExit: (exit: ProcessExit) => void = () => undefined;\n let rejectExit: (error: unknown) => void = () => undefined;\n const exited = new Promise<ProcessExit>((resolve, reject) => {\n resolveExit = resolve;\n rejectExit = reject;\n });\n // A rejected persistence operation is still observable through handle.exited.\n void exited.catch(() => undefined);\n const stdout = streams?.stdout ?? new PassThrough();\n const stderr = streams?.stderr ?? new PassThrough();\n const stdin = streams?.stdin ?? new PassThrough();\n let finishing: Promise<void> | undefined;\n const running: Active = {\n entry,\n finished: false,\n adopted: !streams,\n fail: (error) => rejectExit(error),\n handle: {\n pid: entry.pid,\n stdout,\n stderr,\n stdin,\n exited,\n stop: () =>\n serial(async () => {\n if (!running.finished) await stopEntry(entry);\n }),\n },\n finish(code, sig) {\n finishing ??= (async () => {\n running.finished = true;\n clearInterval(running.timer);\n if (entry.cgroup) {\n const memory = await cgroupMemory(entry.cgroup);\n entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);\n if (memory.oom) running.reason = \"memory-cap\";\n await cleanCgroup(entry.cgroup);\n }\n active.delete(entry.pid);\n entries.delete(entry.pid);\n try {\n await save();\n resolveExit({\n reason: running.reason ?? \"exit\",\n code,\n signal: sig,\n peakMemoryBytes: entry.peakMemoryBytes,\n });\n } catch (error) {\n rejectExit(error);\n throw error;\n }\n if (!streams) {\n (stdout as PassThrough).end();\n (stderr as PassThrough).end();\n stdin.end();\n }\n })();\n return finishing;\n },\n };\n active.set(entry.pid, running);\n return running;\n }\n\n function monitor(running: Active): void {\n if (running.finished) return;\n let sampling = false;\n const sample = async () => {\n if (sampling || running.finished) return;\n sampling = true;\n try {\n const entry = running.entry;\n const current = await identity(entry.pid, true, entry.token);\n if (running.finished) return;\n if (\n current?.start !== entry.start ||\n current.token !== entry.token ||\n current.group !== entry.pid\n ) {\n if (!current) {\n if (running.adopted) await running.finish(null, null);\n } else {\n // A live child that changes its environment or group can no longer\n // be supervised safely. Preserve the entry and surface the failure.\n throw new ProcessNotOwnedError(entry.pid);\n }\n return;\n }\n entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, current.rss);\n if (entry.cgroup) {\n const memory = await cgroupMemory(entry.cgroup);\n if (running.finished) return;\n entry.peakMemoryBytes = Math.max(entry.peakMemoryBytes, memory.peak);\n if (memory.oom) running.reason = \"memory-cap\";\n }\n if (entry.limits.memoryBytes && entry.peakMemoryBytes > entry.limits.memoryBytes)\n await stopEntry(entry, \"memory-cap\");\n else if (entry.limits.timeoutMs && Date.now() - entry.startedAt >= entry.limits.timeoutMs)\n await stopEntry(entry, \"timeout\");\n } finally {\n sampling = false;\n }\n };\n running.timer = setInterval(() => {\n void sample().catch((error: unknown) => {\n // Never signal on an observation error. Surface it to the caller and keep the registry.\n clearInterval(running.timer);\n if (!running.finished) running.fail(error);\n });\n }, pollMs);\n running.timer.unref();\n }\n\n async function launch(\n input: SpawnOptions & { columns?: number; rows?: number },\n terminal: boolean,\n ): Promise<ProcessHandle | PtyHandle> {\n await open();\n if ([...entries.keys()].some((pid) => !active.has(pid)))\n throw new Error(\"Call recover() before spawning with an existing registry\");\n if (\n typeof input.command !== \"string\" ||\n !input.command ||\n typeof input.kind !== \"string\" ||\n !input.kind\n )\n throw new Error(\"kind and command are required\");\n validateFilter({ labels: input.labels });\n if (\n terminal &&\n [input.columns ?? 80, input.rows ?? 24].some(\n (size) => !Number.isSafeInteger(size) || size <= 0,\n )\n )\n throw new Error(\"Terminal dimensions must be positive safe integers\");\n const limits = { ...options.defaults, ...input.limits };\n validateLimits(limits);\n const token = randomBytes(32).toString(\"hex\");\n const {\n file: executable,\n args,\n env,\n } = launchCommand(\n process.platform,\n process.execPath,\n input.command,\n input.args ?? [],\n limits,\n { ...process.env, ...input.env },\n token,\n );\n let pid: number;\n let streams: Pick<ProcessHandle, \"stdin\" | \"stdout\" | \"stderr\">;\n let terminalProcess: Pty | undefined;\n let exit: { code: number | null; signal: string | null } | undefined;\n let spawnError: Error | undefined;\n let running: Active | undefined;\n const onExit = (code: number | null, sig: string | null) => {\n exit = { code, signal: sig };\n if (running) void running.finish(code, sig).catch(() => undefined);\n };\n if (terminal) {\n // Optional peer loaded only here. Structural types follow node-pty's official typings.\n const peer = \"node-pty\";\n const pty = (await import(peer)) as PtyModule;\n terminalProcess = pty.spawn(executable, args, {\n cwd: input.cwd,\n env: Object.fromEntries(\n Object.entries(env).filter((pair): pair is [string, string] => pair[1] !== undefined),\n ),\n cols: input.columns ?? 80,\n rows: input.rows ?? 24,\n name: \"xterm-256color\",\n });\n pid = terminalProcess.pid;\n const output = new PassThrough();\n const errors = new PassThrough();\n const instance = terminalProcess;\n const dataSubscription = instance.onData((data) => output.write(data));\n const inputStream = new Writable({\n write(chunk: Buffer, _encoding, callback) {\n instance.write(chunk.toString());\n callback();\n },\n });\n const exitSubscription = instance.onExit((event) => {\n dataSubscription.dispose();\n exitSubscription.dispose();\n output.end();\n errors.end();\n inputStream.end();\n onExit(event.exitCode, event.signal ? String(event.signal) : null);\n });\n streams = { stdout: output, stderr: errors, stdin: inputStream };\n } else {\n const child = spawnChild(executable, args, {\n cwd: input.cwd,\n env,\n detached: process.platform !== \"win32\",\n windowsHide: true,\n stdio: \"pipe\",\n });\n child.on(\"error\", (error) => {\n spawnError = error;\n });\n // Finish on leader exit so cgroup cleanup can stop remaining members;\n // waiting for close could deadlock on a descendant holding a pipe open.\n child.on(\"exit\", onExit);\n if (!child.pid) throw new Error(\"Could not start process\");\n pid = child.pid;\n streams = { stdout: child.stdout, stderr: child.stderr, stdin: child.stdin };\n }\n let entry: RegistryEntry | undefined;\n try {\n const deadline = Date.now() + 5000;\n let current = await identity(pid, true, token);\n while (!current?.state.startsWith(\"T\") || current.token !== token) {\n if (spawnError) throw spawnError;\n if (exit || Date.now() >= deadline)\n throw new Error(\"Child failed to reach the resource setup gate\");\n await delay(10);\n current = await identity(pid, true, token);\n }\n if (current.group !== pid) throw new Error(\"Child did not create an isolated process group\");\n entry = {\n pid,\n owner: options.owner,\n type: input.kind,\n command: input.command,\n args: [...(input.args ?? [])],\n labels: { ...input.labels },\n startedAt: Date.now(),\n limits,\n enforcement: process.platform === \"win32\" ? \"rss-polling\" : \"rlimit-polling\",\n peakMemoryBytes: current.rss,\n token,\n start: current.start,\n gated: true,\n };\n entry.cgroup = await attachCgroup(pid, token, limits, options.cgroupRoot);\n if (entry.cgroup) entry.enforcement = \"cgroup-v2\";\n entries.set(pid, entry);\n await save();\n running = track(entry, streams);\n await signal(entry, \"SIGCONT\");\n delete entry.gated;\n await save();\n monitor(running);\n if (terminalProcess) {\n const instance = terminalProcess;\n return {\n ...running.handle,\n write: (data: string) => instance.write(data),\n resize: (columns: number, rows: number) => instance.resize(columns, rows),\n };\n }\n return running.handle;\n } catch (error) {\n // This PID was created by this call. Kill only after its random token is verified.\n const current = await identity(pid, true, token);\n if (current?.token === token) {\n const failed = { pid, token, start: current.start };\n if (await owns(failed)) {\n try {\n if (process.platform === \"win32\") await executeCommand(taskkillCommand(pid));\n else process.kill(-pid, \"SIGKILL\");\n } catch {\n /* Already exited. */\n }\n }\n }\n if (entry && !running) {\n entries.delete(pid);\n await save();\n }\n if (entry?.cgroup) await cleanCgroup(entry.cgroup);\n throw error;\n }\n }\n\n const api: ProcessSupervisor = {\n spawn: (input) => serial(() => launch(input, false)),\n spawnPty: (input) => serial(async () => (await launch(input, true)) as PtyHandle),\n list: (filter = {}) =>\n serial(async () => {\n validateFilter(filter);\n await open();\n return [...entries.values()].filter((entry) => matches(entry, filter)).map(info);\n }),\n stop: (filter = {}) =>\n serial(async () => {\n validateFilter(filter);\n await open();\n for (const entry of [...entries.values()].filter((entry) => matches(entry, filter)))\n await stopEntry(entry);\n }),\n stopPid: (pid) =>\n serial(async () => {\n await open();\n const entry = entries.get(pid);\n if (!entry) throw new ProcessNotOwnedError(pid);\n await stopEntry(entry);\n }),\n recover: (recovery = {}) =>\n serial(async () => {\n await open();\n const result: RecoveryResult = { adopted: [], stopped: [], cleared: [] };\n for (const entry of [...entries.values()]) {\n if (active.has(entry.pid)) continue;\n if (!(await owns(entry))) {\n entries.delete(entry.pid);\n result.cleared.push(entry.pid);\n continue;\n }\n // Never trust a persisted filesystem path for cgroup I/O after a restart.\n entry.cgroup = await recoveredCgroup(entry.pid, entry.token);\n entry.enforcement = entry.cgroup\n ? \"cgroup-v2\"\n : process.platform === \"win32\"\n ? \"rss-polling\"\n : \"rlimit-polling\";\n const keep = await recovery.keep?.(info(entry));\n if (keep) {\n const running = track(entry);\n monitor(running);\n if (entry.gated) {\n await signal(entry, \"SIGCONT\");\n delete entry.gated;\n }\n result.adopted.push(entry.pid);\n } else {\n await stopEntry(entry);\n result.stopped.push(entry.pid);\n }\n }\n await save();\n return result;\n }),\n stopAll() {\n shutdown ??= serial(async () => {\n await open();\n closing = true;\n try {\n const errors: unknown[] = [];\n for (const entry of [...entries.values()]) {\n try {\n if (await owns(entry)) await stopEntry(entry);\n else {\n const running = active.get(entry.pid);\n if (running) await running.finish(null, null);\n else entries.delete(entry.pid);\n }\n } catch (error) {\n errors.push(error);\n }\n }\n await save();\n if (errors.length)\n throw new AggregateError(errors, \"Some owned processes could not be stopped\");\n } finally {\n await registry.close();\n }\n });\n return shutdown;\n },\n };\n return api;\n}\n"],"mappings":"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"}
|
package/esm/types.d.mts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
//#region ../../packages/process/src/types.d.ts
|
|
3
|
+
export type ProcessLimits = {
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
memoryBytes?: number;
|
|
6
|
+
/** POSIX RLIMIT_CPU, seconds. */
|
|
7
|
+
cpuSeconds?: number;
|
|
8
|
+
/** Linux RLIMIT_AS; distinct from resident memory, often unsuitable for V8. */
|
|
9
|
+
virtualMemoryBytes?: number;
|
|
10
|
+
};
|
|
11
|
+
export type SupervisorOptions = {
|
|
12
|
+
owner: string;
|
|
13
|
+
/** Absolute or cwd-relative JSON file. One supervisor per registry. */
|
|
14
|
+
registry: string;
|
|
15
|
+
defaults?: ProcessLimits;
|
|
16
|
+
pollIntervalMs?: number;
|
|
17
|
+
stopGraceMs?: number;
|
|
18
|
+
/** Linux delegated cgroup v2 directory; defaults to this process's cgroup. */
|
|
19
|
+
cgroupRoot?: string;
|
|
20
|
+
};
|
|
21
|
+
export type SpawnOptions = {
|
|
22
|
+
/** Workload category, for display only. */
|
|
23
|
+
kind: string;
|
|
24
|
+
command: string;
|
|
25
|
+
args?: string[];
|
|
26
|
+
cwd?: string;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
labels?: Record<string, string>;
|
|
29
|
+
limits?: ProcessLimits;
|
|
30
|
+
};
|
|
31
|
+
export type ProcessFilter = {
|
|
32
|
+
labels?: Record<string, string>;
|
|
33
|
+
};
|
|
34
|
+
export type ProcessExit = {
|
|
35
|
+
reason: "exit" | "timeout" | "memory-cap" | "stopped";
|
|
36
|
+
code: number | null;
|
|
37
|
+
signal: string | null;
|
|
38
|
+
peakMemoryBytes: number;
|
|
39
|
+
};
|
|
40
|
+
export type ProcessInfo = {
|
|
41
|
+
pid: number;
|
|
42
|
+
owner: string;
|
|
43
|
+
type: string;
|
|
44
|
+
command: string;
|
|
45
|
+
args: string[];
|
|
46
|
+
labels: Record<string, string>;
|
|
47
|
+
startedAt: number;
|
|
48
|
+
limits: ProcessLimits;
|
|
49
|
+
enforcement: "cgroup-v2" | "rlimit-polling" | "rss-polling";
|
|
50
|
+
peakMemoryBytes: number;
|
|
51
|
+
};
|
|
52
|
+
export type ProcessHandle = {
|
|
53
|
+
pid: number;
|
|
54
|
+
stdout: Readable;
|
|
55
|
+
stderr: Readable;
|
|
56
|
+
stdin: Writable;
|
|
57
|
+
exited: Promise<ProcessExit>;
|
|
58
|
+
stop(): Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
export type PtyHandle = ProcessHandle & {
|
|
61
|
+
write(data: string): void;
|
|
62
|
+
resize(columns: number, rows: number): void;
|
|
63
|
+
};
|
|
64
|
+
export type RecoveryResult = {
|
|
65
|
+
adopted: number[];
|
|
66
|
+
stopped: number[];
|
|
67
|
+
cleared: number[];
|
|
68
|
+
};
|
|
69
|
+
export type ProcessSupervisor = {
|
|
70
|
+
spawn(options: SpawnOptions): Promise<ProcessHandle>;
|
|
71
|
+
spawnPty(options: SpawnOptions & {
|
|
72
|
+
columns?: number;
|
|
73
|
+
rows?: number;
|
|
74
|
+
}): Promise<PtyHandle>;
|
|
75
|
+
list(filter?: ProcessFilter): Promise<ProcessInfo[]>;
|
|
76
|
+
stop(filter?: ProcessFilter): Promise<void>;
|
|
77
|
+
stopPid(pid: number): Promise<void>;
|
|
78
|
+
recover(options?: {
|
|
79
|
+
keep?: (entry: ProcessInfo) => boolean | Promise<boolean>;
|
|
80
|
+
}): Promise<RecoveryResult>;
|
|
81
|
+
stopAll(): Promise<void>;
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
//# sourceMappingURL=types.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/process/src/types.ts"],"mappings":";;YAEY;EACV;EACA;;EAEA;;EAEA;;YAEU;EACV;;EAEA;EACA,WAAW;EACX;EACA;;EAEA;;YAEU;;EAEV;EACA;EACA;EACA;EACA,MAAM,OAAO;EACb,SAAS;EACT,SAAS;;YAEC;EAAkB,SAAS;;YAC3B;EACV;EACA;EACA;EACA;;YAEU;EACV;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,QAAQ;EACR;EACA;;YAEU;EACV;EACA,QAAQ;EACR,QAAQ;EACR,OAAO;EACP,QAAQ,QAAQ;EAChB,QAAQ;;YAEE,YAAY;EACtB,MAAM;EACN,OAAO,iBAAiB;;YAEd;EAAmB;EAAmB;EAAmB;;YACzD;EACV,MAAM,SAAS,eAAe,QAAQ;EACtC,SAAS,SAAS;IAAiB;IAAkB;MAAkB,QAAQ;EAC/E,KAAK,SAAS,gBAAgB,QAAQ;EACtC,KAAK,SAAS,gBAAgB;EAC9B,QAAQ,cAAc;EACtB,QAAQ;IACN,QAAQ,OAAO,0BAA0B;MACvC,QAAQ;EACZ,WAAW"}
|