local-commander-agent-linux-arm64 0.0.0-stage → 0.3.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +21 -4
- package/runtime/NODE-LICENSE.txt +2732 -0
- package/runtime/agent.mjs +50249 -0
- package/runtime/job-host.mjs +543 -0
- package/runtime/lifecycle.mjs +801 -0
- package/runtime/manifest.json +20 -0
- package/runtime/node +4 -0
- package/runtime/posix-host +0 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// agent/posix/job-host.mjs
|
|
2
|
+
import { spawn, execFile as execFile2 } from "node:child_process";
|
|
3
|
+
import { readFile, lstat, mkdtemp, writeFile, rm, realpath } from "node:fs/promises";
|
|
4
|
+
import { promisify as promisify2 } from "node:util";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path2 from "node:path";
|
|
10
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
11
|
+
|
|
12
|
+
// src/child-environment.ts
|
|
13
|
+
var COMMON_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
14
|
+
"PATH",
|
|
15
|
+
"TEMP",
|
|
16
|
+
"TMP",
|
|
17
|
+
"TMPDIR",
|
|
18
|
+
"HOME",
|
|
19
|
+
"USER",
|
|
20
|
+
"USERNAME",
|
|
21
|
+
"LOGNAME",
|
|
22
|
+
"LANG",
|
|
23
|
+
"LC_ALL",
|
|
24
|
+
"LC_CTYPE",
|
|
25
|
+
"TERM",
|
|
26
|
+
"COLORTERM",
|
|
27
|
+
"NO_COLOR",
|
|
28
|
+
"FORCE_COLOR",
|
|
29
|
+
"CI",
|
|
30
|
+
"DOTNET_ROOT",
|
|
31
|
+
"DOTNET_CLI_HOME"
|
|
32
|
+
]);
|
|
33
|
+
var WINDOWS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
34
|
+
"PATHEXT",
|
|
35
|
+
"SYSTEMROOT",
|
|
36
|
+
"WINDIR",
|
|
37
|
+
"COMSPEC",
|
|
38
|
+
"USERPROFILE",
|
|
39
|
+
"HOMEDRIVE",
|
|
40
|
+
"HOMEPATH",
|
|
41
|
+
"APPDATA",
|
|
42
|
+
"LOCALAPPDATA",
|
|
43
|
+
"PROGRAMFILES",
|
|
44
|
+
"PROGRAMFILES(X86)",
|
|
45
|
+
"PROGRAMW6432",
|
|
46
|
+
"PROCESSOR_ARCHITECTURE",
|
|
47
|
+
"PROCESSOR_ARCHITEW6432",
|
|
48
|
+
"NUMBER_OF_PROCESSORS",
|
|
49
|
+
"OS",
|
|
50
|
+
"USERDOMAIN",
|
|
51
|
+
"PUBLIC"
|
|
52
|
+
]);
|
|
53
|
+
var POSIX_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
54
|
+
"SHELL",
|
|
55
|
+
"XDG_CACHE_HOME",
|
|
56
|
+
"XDG_CONFIG_HOME",
|
|
57
|
+
"XDG_DATA_HOME"
|
|
58
|
+
]);
|
|
59
|
+
var DENIED_KEYS = /* @__PURE__ */ new Set([
|
|
60
|
+
"NODE_OPTIONS",
|
|
61
|
+
"PYTHONPATH",
|
|
62
|
+
"PYTHONHOME",
|
|
63
|
+
"PYTHONSTARTUP",
|
|
64
|
+
"RUBYOPT",
|
|
65
|
+
"PERL5OPT",
|
|
66
|
+
"LD_PRELOAD",
|
|
67
|
+
"LD_LIBRARY_PATH",
|
|
68
|
+
"DYLD_INSERT_LIBRARIES",
|
|
69
|
+
"DYLD_LIBRARY_PATH",
|
|
70
|
+
"GIT_SSH_COMMAND",
|
|
71
|
+
"GIT_ASKPASS",
|
|
72
|
+
"SSH_ASKPASS",
|
|
73
|
+
"NPM_CONFIG_USERCONFIG"
|
|
74
|
+
]);
|
|
75
|
+
var DENIED_SUBSTRINGS = [
|
|
76
|
+
"LCMCP",
|
|
77
|
+
"LOCAL_COMMANDER",
|
|
78
|
+
"PAYPAL",
|
|
79
|
+
"SQL",
|
|
80
|
+
"CONNECTIONSTRING",
|
|
81
|
+
"CONNECTION_STRING",
|
|
82
|
+
"PASSWORD",
|
|
83
|
+
"PASSWD",
|
|
84
|
+
"SECRET",
|
|
85
|
+
"TOKEN",
|
|
86
|
+
"CREDENTIAL",
|
|
87
|
+
"OPENAI",
|
|
88
|
+
"ANTHROPIC",
|
|
89
|
+
"API_KEY",
|
|
90
|
+
"ACCESS_KEY",
|
|
91
|
+
"PRIVATE_KEY"
|
|
92
|
+
];
|
|
93
|
+
function canonicalAllowedKeys(platform) {
|
|
94
|
+
const keys = new Set(COMMON_ALLOWED_KEYS);
|
|
95
|
+
if (platform === "win32") {
|
|
96
|
+
for (const key of WINDOWS_ALLOWED_KEYS) keys.add(key);
|
|
97
|
+
} else {
|
|
98
|
+
for (const key of POSIX_ALLOWED_KEYS) keys.add(key);
|
|
99
|
+
}
|
|
100
|
+
const result = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const key of keys) result.set(platform === "win32" ? key.toUpperCase() : key, key);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function isDeniedKey(key) {
|
|
105
|
+
const upper = key.toUpperCase();
|
|
106
|
+
if (DENIED_KEYS.has(upper)) return true;
|
|
107
|
+
return DENIED_SUBSTRINGS.some((item) => upper.includes(item));
|
|
108
|
+
}
|
|
109
|
+
function buildChildEnvironment(source, platform = process.platform) {
|
|
110
|
+
const allowed = canonicalAllowedKeys(platform);
|
|
111
|
+
const result = {};
|
|
112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const [rawKey, value] of Object.entries(source)) {
|
|
114
|
+
if (value === void 0) continue;
|
|
115
|
+
const lookupKey = platform === "win32" ? rawKey.toUpperCase() : rawKey;
|
|
116
|
+
const outputKey = allowed.get(lookupKey);
|
|
117
|
+
if (!outputKey) continue;
|
|
118
|
+
if (isDeniedKey(rawKey) || isDeniedKey(outputKey)) continue;
|
|
119
|
+
if (seen.has(lookupKey)) continue;
|
|
120
|
+
seen.add(lookupKey);
|
|
121
|
+
result[outputKey] = value;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// agent/src/linux-containment.ts
|
|
127
|
+
import { execFile } from "node:child_process";
|
|
128
|
+
import path from "node:path";
|
|
129
|
+
import { promisify } from "node:util";
|
|
130
|
+
var uuid = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
|
|
131
|
+
var unitName = new RegExp("^lc-command-" + uuid + "\\.service$");
|
|
132
|
+
var bootId = new RegExp("^" + uuid + "$");
|
|
133
|
+
function validLinuxContainment(value) {
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
135
|
+
const v = value;
|
|
136
|
+
return Object.keys(v).length === 5 && Number.isInteger(v.uid) && v.uid > 0 && v.uid <= 4294967295 && typeof v.unit === "string" && unitName.test(v.unit) && typeof v.bootId === "string" && bootId.test(v.bootId) && typeof v.invocationId === "string" && /^[0-9a-f]{32}$/.test(v.invocationId) && !/^0+$/.test(v.invocationId) && typeof v.cgroup === "string" && v.cgroup.length <= 2048 && !v.cgroup.includes("\0") && v.cgroup.startsWith("/user.slice/user-" + v.uid + ".slice/user@" + v.uid + ".service/") && path.posix.normalize(v.cgroup) === v.cgroup && path.posix.basename(v.cgroup) === v.unit;
|
|
137
|
+
}
|
|
138
|
+
var exec = promisify(execFile);
|
|
139
|
+
|
|
140
|
+
// agent/posix/job-host.mjs
|
|
141
|
+
var exec2 = promisify2(execFile2);
|
|
142
|
+
var env = buildChildEnvironment(process.env);
|
|
143
|
+
if (process.platform === "linux") {
|
|
144
|
+
env.XDG_RUNTIME_DIR = "/run/user/" + process.getuid();
|
|
145
|
+
env.DBUS_SESSION_BUS_ADDRESS = "unix:path=" + env.XDG_RUNTIME_DIR + "/bus";
|
|
146
|
+
}
|
|
147
|
+
var unit = "lc-command-" + randomUUID() + ".service";
|
|
148
|
+
var forcedExit;
|
|
149
|
+
var approve;
|
|
150
|
+
var stage = "REQUEST";
|
|
151
|
+
var runner;
|
|
152
|
+
var cgroup;
|
|
153
|
+
var started = false;
|
|
154
|
+
var cancelled = false;
|
|
155
|
+
var timedOut = false;
|
|
156
|
+
var failed = false;
|
|
157
|
+
var finished = false;
|
|
158
|
+
var timer;
|
|
159
|
+
var emit = (value) => process.stdout.write(JSON.stringify(value) + "\n");
|
|
160
|
+
var control = (args) => exec2(
|
|
161
|
+
"/usr/bin/systemctl",
|
|
162
|
+
["--user", "--no-ask-password", ...args, unit],
|
|
163
|
+
{ env, timeout: 2e3, maxBuffer: 8192, encoding: "utf8" }
|
|
164
|
+
);
|
|
165
|
+
var stop = async () => {
|
|
166
|
+
try {
|
|
167
|
+
await control(["stop"]);
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var cancel = () => {
|
|
172
|
+
if (!finished) {
|
|
173
|
+
cancelled = true;
|
|
174
|
+
approve?.(false);
|
|
175
|
+
approve = void 0;
|
|
176
|
+
void stop();
|
|
177
|
+
forcedExit ??= setTimeout(() => process.exit(1), 2500);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
process.stdin.on("end", cancel);
|
|
181
|
+
process.on("SIGTERM", cancel);
|
|
182
|
+
process.on("SIGINT", cancel);
|
|
183
|
+
process.stdout.on("error", () => {
|
|
184
|
+
failed = true;
|
|
185
|
+
cancel();
|
|
186
|
+
});
|
|
187
|
+
function packet(args, childEnv) {
|
|
188
|
+
const pieces = [], number = (n) => {
|
|
189
|
+
const b = Buffer.alloc(4);
|
|
190
|
+
b.writeUInt32BE(n);
|
|
191
|
+
pieces.push(b);
|
|
192
|
+
};
|
|
193
|
+
const strings = (values) => {
|
|
194
|
+
number(values.length);
|
|
195
|
+
for (const value of values) {
|
|
196
|
+
const b = Buffer.from(value);
|
|
197
|
+
if (b.length > 32768 || value.includes("\0")) throw Error();
|
|
198
|
+
number(b.length);
|
|
199
|
+
pieces.push(b);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
strings(args);
|
|
203
|
+
strings(Object.entries(childEnv).map(([key, value]) => key + "=" + value));
|
|
204
|
+
const data = Buffer.concat(pieces);
|
|
205
|
+
if (data.length > 262144) throw Error();
|
|
206
|
+
return data;
|
|
207
|
+
}
|
|
208
|
+
async function emptyGroup() {
|
|
209
|
+
if (!cgroup) return false;
|
|
210
|
+
try {
|
|
211
|
+
return /(?:^|\n)populated 0(?:\n|$)/.test(await readFile(cgroup + "/cgroup.events", "utf8"));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return e.code === "ENOENT";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function validRequest(request) {
|
|
217
|
+
if (!process.getuid() || process.getuid() !== process.geteuid()) throw Error();
|
|
218
|
+
const { executable, args, cwd, timeoutMs, maxProcesses } = request;
|
|
219
|
+
const absolute = (value) => typeof value === "string" && path2.isAbsolute(value) && path2.resolve(value) === value && !value.includes("\0");
|
|
220
|
+
if (!absolute(executable) || !absolute(cwd) || !Array.isArray(args) || args.length > 128 || args.some((x) => typeof x !== "string" || x.length > 8192 || x.includes("\0")) || !Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 864e5 || !Number.isInteger(maxProcesses) || maxProcesses < 1 || maxProcesses > 128) throw Error();
|
|
221
|
+
return { executable, args, cwd, timeoutMs, maxProcesses };
|
|
222
|
+
}
|
|
223
|
+
async function verifiedBridge() {
|
|
224
|
+
const bridge = fileURLToPath(new URL("./posix-host", import.meta.url));
|
|
225
|
+
stage = "BRIDGE";
|
|
226
|
+
const stat = await lstat(bridge);
|
|
227
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 18 || stat.uid !== 0 && stat.uid !== process.getuid()) throw Error();
|
|
228
|
+
if (cancelled) throw Error();
|
|
229
|
+
return bridge;
|
|
230
|
+
}
|
|
231
|
+
async function execute(request) {
|
|
232
|
+
if (process.platform === "darwin") return executeDarwin(request);
|
|
233
|
+
if (process.platform !== "linux") throw Error();
|
|
234
|
+
const { executable, args, cwd, timeoutMs, maxProcesses } = validRequest(request);
|
|
235
|
+
const payload = packet([executable, ...args], buildChildEnvironment(process.env));
|
|
236
|
+
const bridge = await verifiedBridge();
|
|
237
|
+
stage = "CGROUP";
|
|
238
|
+
runner = spawn(
|
|
239
|
+
"/usr/bin/systemd-run",
|
|
240
|
+
[
|
|
241
|
+
"--user",
|
|
242
|
+
"--no-ask-password",
|
|
243
|
+
"--quiet",
|
|
244
|
+
"--wait",
|
|
245
|
+
"--pipe",
|
|
246
|
+
"--collect",
|
|
247
|
+
"--expand-environment=no",
|
|
248
|
+
"--unit=" + unit,
|
|
249
|
+
"--description=Local Commander bounded command",
|
|
250
|
+
"--service-type=exec",
|
|
251
|
+
"--working-directory=" + cwd,
|
|
252
|
+
"--property=TasksMax=" + maxProcesses,
|
|
253
|
+
"--property=KillMode=control-group",
|
|
254
|
+
"--property=SendSIGKILL=yes",
|
|
255
|
+
"--property=TimeoutStopSec=1s",
|
|
256
|
+
"--property=RuntimeMaxSec=" + (timeoutMs + 5e3) / 1e3,
|
|
257
|
+
"--",
|
|
258
|
+
bridge
|
|
259
|
+
],
|
|
260
|
+
{ env, stdio: ["pipe", "pipe", "pipe"], shell: false }
|
|
261
|
+
);
|
|
262
|
+
let runnerClosed = false, prefix = Buffer.alloc(0);
|
|
263
|
+
const closed = new Promise((resolve, reject) => {
|
|
264
|
+
runner.once("error", reject);
|
|
265
|
+
runner.once("close", (code2) => {
|
|
266
|
+
runnerClosed = true;
|
|
267
|
+
resolve(code2);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
void closed.catch(() => {
|
|
271
|
+
});
|
|
272
|
+
const output = (stream, bytes) => {
|
|
273
|
+
for (let i = 0; i < bytes.length; i += 4096) {
|
|
274
|
+
if (!emit({ type: stream, data: bytes.subarray(i, i + 4096).toString("base64") })) {
|
|
275
|
+
runner.stdout.pause();
|
|
276
|
+
runner.stderr.pause();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
process.stdout.on("drain", () => {
|
|
281
|
+
runner.stdout.resume();
|
|
282
|
+
runner.stderr.resume();
|
|
283
|
+
});
|
|
284
|
+
runner.stdout.on("data", (bytes) => {
|
|
285
|
+
if (!started) {
|
|
286
|
+
prefix = Buffer.concat([prefix, bytes]);
|
|
287
|
+
const newline = prefix.indexOf(10);
|
|
288
|
+
if (newline < 0) {
|
|
289
|
+
if (prefix.length > 32) {
|
|
290
|
+
failed = true;
|
|
291
|
+
cancel();
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const text = prefix.subarray(0, newline).toString();
|
|
296
|
+
if (!/^[1-9][0-9]{0,9}$/.test(text)) {
|
|
297
|
+
failed = true;
|
|
298
|
+
cancel();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
started = true;
|
|
302
|
+
emit({ type: "started", pid: Number(text) });
|
|
303
|
+
bytes = prefix.subarray(newline + 1);
|
|
304
|
+
prefix = Buffer.alloc(0);
|
|
305
|
+
}
|
|
306
|
+
output("stdout", bytes);
|
|
307
|
+
});
|
|
308
|
+
runner.stderr.on("data", (bytes) => output("stderr", bytes));
|
|
309
|
+
runner.stdin.on("error", () => {
|
|
310
|
+
failed = true;
|
|
311
|
+
void stop();
|
|
312
|
+
});
|
|
313
|
+
const bootId2 = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim();
|
|
314
|
+
let containment;
|
|
315
|
+
const setupDeadline = Date.now() + 4e3;
|
|
316
|
+
while (!cgroup && !runnerClosed && !cancelled && Date.now() < setupDeadline) {
|
|
317
|
+
try {
|
|
318
|
+
const { stdout } = await control(["show", "--property=ControlGroup", "--property=TasksMax", "--property=ActiveState", "--property=InvocationID"]);
|
|
319
|
+
const fields = Object.fromEntries(stdout.trim().split("\n").map((line) => {
|
|
320
|
+
const i = line.indexOf("=");
|
|
321
|
+
return [line.slice(0, i), line.slice(i + 1)];
|
|
322
|
+
}));
|
|
323
|
+
const proof = { uid: process.getuid(), unit, cgroup: fields.ControlGroup, bootId: bootId2, invocationId: fields.InvocationID };
|
|
324
|
+
if (fields.ActiveState === "active" && fields.TasksMax === String(maxProcesses) && validLinuxContainment(proof)) {
|
|
325
|
+
const directory = "/sys/fs/cgroup" + fields.ControlGroup;
|
|
326
|
+
if ((await readFile(directory + "/pids.max", "utf8")).trim() === String(maxProcesses)) {
|
|
327
|
+
cgroup = directory;
|
|
328
|
+
containment = proof;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
if (!cgroup) await delay(25);
|
|
334
|
+
}
|
|
335
|
+
if (!cgroup || cancelled || runnerClosed) {
|
|
336
|
+
await stop();
|
|
337
|
+
throw Error();
|
|
338
|
+
}
|
|
339
|
+
const approval = new Promise((resolve) => {
|
|
340
|
+
approve = resolve;
|
|
341
|
+
});
|
|
342
|
+
emit({ type: "containment", containment });
|
|
343
|
+
const approved = await Promise.race([approval, delay(1e4, false, { ref: false })]);
|
|
344
|
+
approve = void 0;
|
|
345
|
+
if (!approved || cancelled || runnerClosed) {
|
|
346
|
+
await stop();
|
|
347
|
+
throw Error();
|
|
348
|
+
}
|
|
349
|
+
timer = setTimeout(() => {
|
|
350
|
+
timedOut = true;
|
|
351
|
+
void stop();
|
|
352
|
+
forcedExit ??= setTimeout(() => process.exit(1), 2500);
|
|
353
|
+
}, timeoutMs);
|
|
354
|
+
stage = "EXECUTION";
|
|
355
|
+
runner.stdin.end(payload);
|
|
356
|
+
const code = await closed;
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
await stop();
|
|
359
|
+
stage = "CLEANUP";
|
|
360
|
+
if (failed || !started || !Number.isInteger(code) || !await emptyGroup()) throw Error();
|
|
361
|
+
finished = true;
|
|
362
|
+
emit({ type: "exit", exitCode: code, timedOut, cancelled, treeTerminated: true });
|
|
363
|
+
}
|
|
364
|
+
var xml = (value) => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
365
|
+
async function executeDarwin(request) {
|
|
366
|
+
const { executable, args, cwd, timeoutMs, maxProcesses } = validRequest(request);
|
|
367
|
+
const uid = process.getuid();
|
|
368
|
+
const payload = packet([executable, ...args], buildChildEnvironment(process.env));
|
|
369
|
+
const bridge = await verifiedBridge();
|
|
370
|
+
const helper = async (...values) => (await exec2(bridge, values, { env, timeout: 7e3, maxBuffer: 1024, encoding: "utf8" })).stdout.trim();
|
|
371
|
+
const launchctl = (values) => exec2("/bin/launchctl", values, { env, timeout: 5e3, maxBuffer: 65536, encoding: "utf8" });
|
|
372
|
+
const own = await helper("--coalition", String(process.pid));
|
|
373
|
+
const bootSession = (await exec2("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], { env, timeout: 2e3, maxBuffer: 1024, encoding: "utf8" })).stdout.trim();
|
|
374
|
+
if (!/^[1-9][0-9]{0,19}$/.test(own) || cancelled) throw Error();
|
|
375
|
+
stage = "CGROUP";
|
|
376
|
+
const directory = await mkdtemp(path2.join(await realpath(os.tmpdir()), "lc-"));
|
|
377
|
+
const socketPath = path2.join(directory, "s"), plist = path2.join(directory, "job.plist");
|
|
378
|
+
const label = "app.localcmd.command." + randomUUID(), job = "gui/" + uid + "/" + label;
|
|
379
|
+
const roles = /* @__PURE__ */ new Map();
|
|
380
|
+
let lines = "", hello, code, empty, coalition, loaded = false, sent = false, reaping;
|
|
381
|
+
const streams = () => [roles.get("o"), roles.get("e")].filter(Boolean);
|
|
382
|
+
const output = (stream, bytes) => {
|
|
383
|
+
for (let i = 0; i < bytes.length; i += 4096) {
|
|
384
|
+
if (!emit({ type: stream, data: bytes.subarray(i, i + 4096).toString("base64") })) for (const s of streams()) s.pause();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
process.stdout.on("drain", () => {
|
|
388
|
+
for (const s of streams()) s.resume();
|
|
389
|
+
});
|
|
390
|
+
const reject = () => {
|
|
391
|
+
failed = true;
|
|
392
|
+
cancel();
|
|
393
|
+
};
|
|
394
|
+
const onControl = (bytes) => {
|
|
395
|
+
lines += bytes.toString("latin1");
|
|
396
|
+
if (lines.length > 256) {
|
|
397
|
+
reject();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
for (let i; (i = lines.indexOf("\n")) >= 0; ) {
|
|
401
|
+
const line = lines.slice(0, i);
|
|
402
|
+
lines = lines.slice(i + 1);
|
|
403
|
+
let m;
|
|
404
|
+
if (!hello && (m = /^hello ([1-9][0-9]{0,9}) ([1-9][0-9]{0,19})$/.exec(line))) hello = { pid: Number(m[1]), coalition: m[2] };
|
|
405
|
+
else if (sent && !started && (m = /^started ([1-9][0-9]{0,9})$/.exec(line))) {
|
|
406
|
+
started = true;
|
|
407
|
+
emit({ type: "started", pid: Number(m[1]) });
|
|
408
|
+
} else if (sent && code === void 0 && (m = /^exit ([0-9]{1,3})$/.exec(line))) code = Number(m[1]);
|
|
409
|
+
else if (code !== void 0 && empty === void 0 && (m = /^empty ([01])$/.exec(line))) empty = m[1] === "1";
|
|
410
|
+
else {
|
|
411
|
+
reject();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const server = net.createServer((socket) => {
|
|
417
|
+
socket.on("error", () => {
|
|
418
|
+
});
|
|
419
|
+
const claim = () => {
|
|
420
|
+
const role = socket.read(1);
|
|
421
|
+
if (role === null) {
|
|
422
|
+
socket.once("readable", claim);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const name = role.toString("latin1");
|
|
426
|
+
if (!["c", "o", "e"].includes(name) || roles.has(name)) {
|
|
427
|
+
socket.destroy();
|
|
428
|
+
reject();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
roles.set(name, socket);
|
|
432
|
+
socket.on("data", name === "c" ? onControl : (bytes) => output(name === "o" ? "stdout" : "stderr", bytes));
|
|
433
|
+
};
|
|
434
|
+
socket.once("readable", claim);
|
|
435
|
+
});
|
|
436
|
+
stop = async () => {
|
|
437
|
+
const controlSocket = roles.get("c");
|
|
438
|
+
if (sent && controlSocket && !controlSocket.destroyed) controlSocket.write("k", () => {
|
|
439
|
+
});
|
|
440
|
+
if (coalition && !finished) reaping ??= delay(800).then(() => finished ? void 0 : helper("--reap", coalition).catch(() => {
|
|
441
|
+
}));
|
|
442
|
+
await reaping;
|
|
443
|
+
};
|
|
444
|
+
try {
|
|
445
|
+
await new Promise((resolve, fail) => {
|
|
446
|
+
server.once("error", fail);
|
|
447
|
+
server.listen(socketPath, resolve);
|
|
448
|
+
});
|
|
449
|
+
await writeFile(plist, '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>' + xml(label) + "</string><key>ProgramArguments</key><array>" + [bridge, "--launchd", socketPath, String(maxProcesses), String(timeoutMs)].map((v) => "<string>" + xml(v) + "</string>").join("") + "</array><key>WorkingDirectory</key><string>" + xml(cwd) + "</string><key>RunAtLoad</key><true/><key>LaunchOnlyOnce</key><true/><key>KeepAlive</key><false/><key>ProcessType</key><string>Interactive</string></dict></plist>\n", { mode: 384, flag: "wx" });
|
|
450
|
+
if (cancelled) throw Error();
|
|
451
|
+
await launchctl(["bootstrap", "gui/" + uid, plist]);
|
|
452
|
+
loaded = true;
|
|
453
|
+
const setupDeadline = Date.now() + 4e3;
|
|
454
|
+
while (!(hello && roles.size === 3) && !cancelled && !failed && Date.now() < setupDeadline) await delay(25);
|
|
455
|
+
if (!hello || roles.size !== 3 || cancelled || failed) throw Error();
|
|
456
|
+
const printed = /^\tpid = ([1-9][0-9]{0,9})$/m.exec((await launchctl(["print", job])).stdout);
|
|
457
|
+
if (!printed || Number(printed[1]) !== hello.pid || hello.coalition === own || await helper("--coalition", String(hello.pid)) !== hello.coalition) throw Error();
|
|
458
|
+
coalition = hello.coalition;
|
|
459
|
+
if (cancelled) throw Error();
|
|
460
|
+
const approval = new Promise((resolve) => {
|
|
461
|
+
approve = resolve;
|
|
462
|
+
});
|
|
463
|
+
emit({ type: "containment", containment: { uid, label, coalition, bootSession } });
|
|
464
|
+
const approved = await Promise.race([approval, delay(1e4, false, { ref: false })]);
|
|
465
|
+
approve = void 0;
|
|
466
|
+
if (!approved || cancelled || failed) throw Error();
|
|
467
|
+
const closed = new Promise((resolve) => roles.get("c").once("close", resolve));
|
|
468
|
+
const drained = Promise.all(streams().map((s) => new Promise((resolve) => s.once("close", resolve))));
|
|
469
|
+
timer = setTimeout(() => {
|
|
470
|
+
timedOut = true;
|
|
471
|
+
void stop();
|
|
472
|
+
forcedExit ??= setTimeout(() => process.exit(1), 2500);
|
|
473
|
+
}, timeoutMs);
|
|
474
|
+
stage = "EXECUTION";
|
|
475
|
+
sent = true;
|
|
476
|
+
roles.get("c").write(payload);
|
|
477
|
+
await closed;
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
await Promise.race([drained, delay(2e3)]);
|
|
480
|
+
stage = "CLEANUP";
|
|
481
|
+
} finally {
|
|
482
|
+
if (coalition) await helper("--reap", coalition).catch(() => {
|
|
483
|
+
failed = true;
|
|
484
|
+
});
|
|
485
|
+
if (loaded) await launchctl(["bootout", job]).catch(() => {
|
|
486
|
+
});
|
|
487
|
+
server.close();
|
|
488
|
+
for (const socket of roles.values()) socket.destroy();
|
|
489
|
+
await rm(directory, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
if (failed || !started || code === void 0 || empty !== true || await helper("--count", coalition) !== "0") throw Error();
|
|
492
|
+
finished = true;
|
|
493
|
+
emit({ type: "exit", exitCode: code, timedOut, cancelled, treeTerminated: true });
|
|
494
|
+
}
|
|
495
|
+
var buffer = "";
|
|
496
|
+
var received = false;
|
|
497
|
+
var deadline = setTimeout(() => {
|
|
498
|
+
failed = true;
|
|
499
|
+
cancel();
|
|
500
|
+
process.exitCode = 1;
|
|
501
|
+
process.stdin.destroy();
|
|
502
|
+
}, 15e3);
|
|
503
|
+
process.stdin.setEncoding("utf8");
|
|
504
|
+
process.stdin.on("data", (chunk) => {
|
|
505
|
+
buffer += chunk;
|
|
506
|
+
if (Buffer.byteLength(buffer) > 65536) {
|
|
507
|
+
failed = true;
|
|
508
|
+
cancel();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
for (; ; ) {
|
|
512
|
+
const index = buffer.indexOf("\n");
|
|
513
|
+
if (index < 0) break;
|
|
514
|
+
const line = buffer.slice(0, index);
|
|
515
|
+
buffer = buffer.slice(index + 1);
|
|
516
|
+
if (received) {
|
|
517
|
+
if (line === "cancel") cancel();
|
|
518
|
+
else if (line === "continue" && approve) {
|
|
519
|
+
const accept = approve;
|
|
520
|
+
approve = void 0;
|
|
521
|
+
accept(true);
|
|
522
|
+
} else {
|
|
523
|
+
failed = true;
|
|
524
|
+
cancel();
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
received = true;
|
|
529
|
+
clearTimeout(deadline);
|
|
530
|
+
Promise.resolve().then(() => execute(JSON.parse(line))).catch(async () => {
|
|
531
|
+
failed = true;
|
|
532
|
+
await stop();
|
|
533
|
+
emit({ type: "error", stage });
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}).finally(() => {
|
|
536
|
+
clearTimeout(timer);
|
|
537
|
+
clearTimeout(deadline);
|
|
538
|
+
clearTimeout(forcedExit);
|
|
539
|
+
finished = true;
|
|
540
|
+
process.stdin.destroy();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
});
|