@outerlayer/cli 0.5.0 → 0.6.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 +17 -0
- package/README.md +9 -0
- package/dist/{agent-setup-IFH3FB5X.js → agent-setup-USZ2SAXC.js} +2 -2
- package/dist/build-info.json +1 -1
- package/dist/{check-AXCXDYO2.js → check-CCVPBN7A.js} +26 -14
- package/dist/{chunk-5Y3QQRUH.js → chunk-3DD6AOWC.js} +37 -0
- package/dist/{chunk-WOBNRVQT.js → chunk-3DEVYTNQ.js} +1 -1
- package/dist/{chunk-L24G4PLP.js → chunk-43J3M6KB.js} +486 -21
- package/dist/{chunk-FC7TMRNA.js → chunk-4NLKY4WR.js} +2 -2
- package/dist/{lima-XN65D7GN.js → chunk-77MM5RYD.js} +0 -1
- package/dist/{chunk-D2KWEAN6.js → chunk-7DBI6SNT.js} +1 -1
- package/dist/{chunk-QDQEUVUF.js → chunk-AIW6GLET.js} +1 -1
- package/dist/{chunk-EX6M5JPR.js → chunk-ANK2B36Y.js} +131 -15
- package/dist/{chunk-RMAZ2NWA.js → chunk-BNMCXPG7.js} +14 -2
- package/dist/{chunk-UFFXNXLQ.js → chunk-FA2BOX4O.js} +31 -29
- package/dist/chunk-FBTE2A2X.js +38 -0
- package/dist/{chunk-DVDEBNQJ.js → chunk-GQD22KKN.js} +17 -6
- package/dist/{chunk-BCJNHMZT.js → chunk-HHJDN6AU.js} +18 -3
- package/dist/{chunk-JLDZTG3A.js → chunk-IASWYRVG.js} +1 -1
- package/dist/{install-cmd-7WXOIFDO.js → chunk-JWOQNNN2.js} +0 -1
- package/dist/{chunk-ZLB7GCTY.js → chunk-MIGEDH5P.js} +19 -11
- package/dist/{chunk-OHFDWLJP.js → chunk-MXB5VB4Y.js} +18 -6
- package/dist/chunk-NQXFAGHU.js +211 -0
- package/dist/chunk-OUUINEYB.js +27 -0
- package/dist/{chunk-OR6AEEDO.js → chunk-PMAMOSTP.js} +3 -3
- package/dist/{chunk-RJ467PDQ.js → chunk-RBOMWNOG.js} +1 -1
- package/dist/{chunk-4NMBDRBS.js → chunk-RZSF5B46.js} +289 -130
- package/dist/chunk-T6OIJQRT.js +31 -0
- package/dist/chunk-UGP76IZ5.js +34 -0
- package/dist/{chunk-EGABNHZG.js → chunk-UMFWVGJO.js} +148 -18
- package/dist/{chunk-RWOTHXYE.js → chunk-VMJ3MMBX.js} +206 -71
- package/dist/chunk-YYYRBYWG.js +32 -0
- package/dist/chunk-ZILGTTY6.js +75 -0
- package/dist/{cli-NNWPFP5S.js → cli-7WCNPBYC.js} +147 -69
- package/dist/command-ARFK3OYF.js +162 -0
- package/dist/{config-XVJYZ4IQ.js → config-HL2AIR3K.js} +1 -1
- package/dist/{connect-cmd-OJBFHZWP.js → connect-cmd-PCESZLTR.js} +4 -4
- package/dist/{context-adopt-UMA4O5NY.js → context-adopt-G5JAELE7.js} +2 -2
- package/dist/{context-materialize-G2FUFC72.js → context-materialize-3HLPZAF6.js} +2 -2
- package/dist/{dist-WDV7U7UJ.js → dist-RDLP4GGJ.js} +1 -1
- package/dist/{doctor-J3GIMR7K.js → doctor-VOMYOHQU.js} +47 -13
- package/dist/{emit-artifact-cmd-FDWYKURA.js → emit-artifact-cmd-TBF46VXZ.js} +6 -7
- package/dist/{emit-criteria-cmd-OCXHLK34.js → emit-criteria-cmd-3427XKFW.js} +3 -3
- package/dist/{emit-finding-cmd-POW7PKIA.js → emit-finding-cmd-GSI75K36.js} +3 -3
- package/dist/{emit-result-cmd-ID35SH53.js → emit-result-cmd-3PRQKB55.js} +6 -7
- package/dist/{hook-fast-EYENPK6F.js → hook-fast-NJBBZQ5Q.js} +1 -1
- package/dist/index.js +3 -2
- package/dist/{init-CZX7ZDWF.js → init-OHYFIKZD.js} +27 -56
- package/dist/{init-3ZQLFPNC.js → init-WVR4BZQ4.js} +8 -8
- package/dist/{init-cmd-W5NOLQPX.js → init-cmd-HMFNIFPI.js} +7 -7
- package/dist/inspect-XAUSB7L3.js +63 -0
- package/dist/install-DSUZE2TJ.js +13 -0
- package/dist/install-cmd-453C7IZT.js +8 -0
- package/dist/lima-7NPCNGWT.js +6 -0
- package/dist/{loop-VGM772I7.js → loop-UAJB34KA.js} +26 -11
- package/dist/{machine-5WEJA36F.js → machine-2JOWGRSZ.js} +7 -7
- package/dist/record-C43U4PKS.js +7 -0
- package/dist/session-sender-ED26V45B.js +28 -0
- package/dist/{status-KBDVUYQ3.js → status-6WNXX2O7.js} +42 -10
- package/dist/{sync-cmd-SWQ5KLPL.js → sync-cmd-YONVGUYS.js} +6 -7
- package/dist/{watch-3YW6Z3DQ.js → watch-HHKAF26L.js} +6 -7
- package/dist/{work-claim-cmd-BQPPURBW.js → work-claim-cmd-LXBZTMJA.js} +6 -6
- package/dist/{work-cmd-NUR4CAG6.js → work-cmd-OVNF44FY.js} +3 -3
- package/dist/{work-comment-cmd-6TTTYGHM.js → work-comment-cmd-6DGQNTAH.js} +3 -3
- package/dist/{work-launch-YI4CJDAP.js → work-launch-R5HTAP6X.js} +1 -1
- package/dist/{work-open-pr-cmd-KZ7QQFLD.js → work-open-pr-cmd-FCZHFHZS.js} +4 -4
- package/dist/{work-pr-cmd-QNUBE6IQ.js → work-pr-cmd-QJJVHZDC.js} +2 -2
- package/package.json +2 -1
- package/dist/chunk-ZOC7JSYC.js +0 -83
- package/dist/{chunk-XDDW4FRS.js → chunk-OIRNH6UO.js} +1 -1
|
@@ -7,23 +7,23 @@ import { createDockerHost, childEnvironment, spawnRun } from './chunk-774HEQL6.j
|
|
|
7
7
|
import { RecipeError } from './chunk-QOZKPLVJ.js';
|
|
8
8
|
import { execCapture } from './chunk-W4SQCOZU.js';
|
|
9
9
|
import { GUEST_CID } from './chunk-B2G7JHHB.js';
|
|
10
|
-
import { WorkCommandError, postJson } from './chunk-
|
|
10
|
+
import { WorkCommandError, postJson } from './chunk-BNMCXPG7.js';
|
|
11
11
|
import { parseDuKilobytes, hostAllowed } from './chunk-F6GFZXZ3.js';
|
|
12
12
|
import { RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, AGENT_CREDENTIAL_MISSING, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_PORT, CLAUDE_SOCKET, DESTINATION_BASE_PORT } from './chunk-AUFG23AA.js';
|
|
13
|
-
import { defaultGitRunner, materializeContext } from './chunk-
|
|
13
|
+
import { defaultGitRunner, materializeContext } from './chunk-OIRNH6UO.js';
|
|
14
14
|
import { readContextManifest, readSourceCache } from './chunk-WIOZAJ2W.js';
|
|
15
15
|
import { signingFetchFor } from './chunk-BCLJSQCV.js';
|
|
16
16
|
import { signerFor } from './chunk-M5POMKKW.js';
|
|
17
|
-
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
|
|
18
|
-
import { relative, join, dirname, basename } from 'path';
|
|
17
|
+
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, runnerDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
|
|
18
|
+
import { relative, join, dirname, resolve, basename } from 'path';
|
|
19
|
+
import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, accessSync, constants, readdirSync, statSync, rmdirSync, realpathSync, renameSync, lstatSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
|
|
19
20
|
import { spawnSync, spawn } from 'child_process';
|
|
20
|
-
import {
|
|
21
|
-
import { existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, renameSync, lstatSync, readFileSync, unlinkSync, readdirSync, openSync, closeSync, mkdtempSync, createWriteStream, statSync, createReadStream } from 'fs';
|
|
21
|
+
import { createHash, randomBytes } from 'crypto';
|
|
22
22
|
import { createServer, request } from 'http';
|
|
23
23
|
import { request as request$1 } from 'https';
|
|
24
24
|
import { Transform } from 'stream';
|
|
25
25
|
import { pipeline, finished } from 'stream/promises';
|
|
26
|
-
import { createGunzip, gunzipSync, constants } from 'zlib';
|
|
26
|
+
import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
|
|
27
27
|
import { isIP, connect, createServer as createServer$1 } from 'net';
|
|
28
28
|
import { lookup } from 'dns/promises';
|
|
29
29
|
import { networkInterfaces } from 'os';
|
|
@@ -86,7 +86,7 @@ function isRetryable(err) {
|
|
|
86
86
|
}
|
|
87
87
|
async function retrySend(send, deps = {}) {
|
|
88
88
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
89
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((
|
|
89
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
90
90
|
const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
|
|
91
91
|
const startedMs = now().getTime();
|
|
92
92
|
let attempts = 0;
|
|
@@ -255,6 +255,111 @@ var BuildTokens = class {
|
|
|
255
255
|
);
|
|
256
256
|
}
|
|
257
257
|
};
|
|
258
|
+
var BUILD_PREFIX = "build-";
|
|
259
|
+
var LEAF = "runner";
|
|
260
|
+
function serviceDirectory(selfCgroup, mount) {
|
|
261
|
+
const line = selfCgroup?.split("\n").find((l) => l.startsWith("0::"));
|
|
262
|
+
if (line === void 0) return void 0;
|
|
263
|
+
const path = line.slice(3).trim();
|
|
264
|
+
if (!path.endsWith(".service")) return void 0;
|
|
265
|
+
return join(mount, path);
|
|
266
|
+
}
|
|
267
|
+
function prepareRunnerCgroup(setup) {
|
|
268
|
+
if (setup.invocationId === void 0 || setup.invocationId === "") return void 0;
|
|
269
|
+
const root = serviceDirectory(setup.selfCgroup, setup.mount);
|
|
270
|
+
if (root === void 0) return void 0;
|
|
271
|
+
const { fs } = setup;
|
|
272
|
+
if (!fs.exists(root) || !fs.writable(join(root, "cgroup.procs"))) return void 0;
|
|
273
|
+
if (!(fs.read(join(root, "cgroup.controllers")) ?? "").split(/\s+/).includes("memory")) return void 0;
|
|
274
|
+
for (const name of fs.list(root)) {
|
|
275
|
+
if (!name.startsWith(BUILD_PREFIX)) continue;
|
|
276
|
+
try {
|
|
277
|
+
fs.rmdir(join(root, name));
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!fs.exists(join(root, LEAF))) fs.mkdir(join(root, LEAF));
|
|
282
|
+
fs.write(join(root, LEAF, "cgroup.procs"), String(setup.pid));
|
|
283
|
+
fs.write(join(root, "cgroup.subtree_control"), "+memory");
|
|
284
|
+
return { root };
|
|
285
|
+
}
|
|
286
|
+
function createBuildCgroup(fs, root, buildId, buildMemoryBytes) {
|
|
287
|
+
const path = join(root, `${BUILD_PREFIX}${buildId}`);
|
|
288
|
+
const firecrackerOverheadBytes = 256 * 1024 ** 2;
|
|
289
|
+
fs.mkdir(path);
|
|
290
|
+
try {
|
|
291
|
+
fs.write(join(path, "memory.max"), String(buildMemoryBytes + firecrackerOverheadBytes));
|
|
292
|
+
fs.write(join(path, "memory.swap.max"), "0");
|
|
293
|
+
} catch (err) {
|
|
294
|
+
removeBuildCgroup(fs, path);
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
return path;
|
|
298
|
+
}
|
|
299
|
+
function oomKills(fs, path) {
|
|
300
|
+
const match = /^oom_kill (\d+)$/m.exec(fs.read(join(path, "memory.events")) ?? "");
|
|
301
|
+
return match === null ? 0 : Number(match[1]);
|
|
302
|
+
}
|
|
303
|
+
function removeBuildCgroup(fs, path) {
|
|
304
|
+
try {
|
|
305
|
+
fs.rmdir(path);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (err.code !== "ENOENT") throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function realCgroupFs() {
|
|
311
|
+
return {
|
|
312
|
+
exists: (path) => existsSync(path),
|
|
313
|
+
read: (path) => {
|
|
314
|
+
try {
|
|
315
|
+
return readFileSync(path, "utf8");
|
|
316
|
+
} catch {
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
write: (path, data) => writeFileSync(path, data),
|
|
321
|
+
mkdir: (path) => mkdirSync(path),
|
|
322
|
+
rmdir: (path) => rmdirSync(path),
|
|
323
|
+
list: (path) => readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory()),
|
|
324
|
+
writable: (path) => {
|
|
325
|
+
try {
|
|
326
|
+
accessSync(path, constants.W_OK);
|
|
327
|
+
return true;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
var delegatedRoot;
|
|
335
|
+
function setUpRunnerCgroup(over = {}) {
|
|
336
|
+
let selfCgroup;
|
|
337
|
+
try {
|
|
338
|
+
selfCgroup = readFileSync("/proc/self/cgroup", "utf8");
|
|
339
|
+
} catch {
|
|
340
|
+
selfCgroup = void 0;
|
|
341
|
+
}
|
|
342
|
+
let refusal;
|
|
343
|
+
try {
|
|
344
|
+
const prepared = prepareRunnerCgroup({ fs: realCgroupFs(), selfCgroup, pid: process.pid, invocationId: process.env.INVOCATION_ID, mount: "/sys/fs/cgroup", ...over });
|
|
345
|
+
delegatedRoot = prepared?.root;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
delegatedRoot = void 0;
|
|
348
|
+
refusal = err instanceof Error ? err.message : String(err);
|
|
349
|
+
}
|
|
350
|
+
return { delegated: delegatedRoot !== void 0, ...refusal === void 0 ? {} : { refusal } };
|
|
351
|
+
}
|
|
352
|
+
function realCgroupPort() {
|
|
353
|
+
const fs = realCgroupFs();
|
|
354
|
+
return {
|
|
355
|
+
create: (buildId, bytes) => {
|
|
356
|
+
if (delegatedRoot === void 0) return void 0;
|
|
357
|
+
return createBuildCgroup(fs, delegatedRoot, buildId, bytes);
|
|
358
|
+
},
|
|
359
|
+
oomKills: (path) => oomKills(fs, path),
|
|
360
|
+
remove: (path) => removeBuildCgroup(fs, path)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
258
363
|
var BranchPrepareError = class extends Error {
|
|
259
364
|
};
|
|
260
365
|
var OWN_PREFIX = "outerlayer/";
|
|
@@ -356,6 +461,119 @@ function lifecycleSteps(lifecycle) {
|
|
|
356
461
|
function lifecycleFailureReason(step, exitCode) {
|
|
357
462
|
return `lifecycle command ${step.name} (${step.source}) exited ${exitCode}`;
|
|
358
463
|
}
|
|
464
|
+
var BUILD_LABEL = "ai.outerlayer.build";
|
|
465
|
+
var RUNNER_LABEL = "ai.outerlayer.runner";
|
|
466
|
+
function resolvedRunnerDir(configPath) {
|
|
467
|
+
try {
|
|
468
|
+
return realpathSync(runnerDir(configPath));
|
|
469
|
+
} catch {
|
|
470
|
+
try {
|
|
471
|
+
return join(realpathSync(dirname(resolve(configPath))), "runner");
|
|
472
|
+
} catch {
|
|
473
|
+
return resolve(runnerDir(configPath));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function runnerLabelValue(configPath) {
|
|
478
|
+
return createHash("sha256").update(resolvedRunnerDir(configPath)).digest("hex").slice(0, 12);
|
|
479
|
+
}
|
|
480
|
+
function hardened(input) {
|
|
481
|
+
const { memory, cpus, pids } = input.limits;
|
|
482
|
+
return [
|
|
483
|
+
"--label",
|
|
484
|
+
`${BUILD_LABEL}=${input.buildId}`,
|
|
485
|
+
"--label",
|
|
486
|
+
`${RUNNER_LABEL}=${input.runnerId}`,
|
|
487
|
+
"--network",
|
|
488
|
+
"none",
|
|
489
|
+
"--cap-drop",
|
|
490
|
+
"ALL",
|
|
491
|
+
"--security-opt",
|
|
492
|
+
"no-new-privileges",
|
|
493
|
+
"--read-only",
|
|
494
|
+
"--user",
|
|
495
|
+
input.user,
|
|
496
|
+
// The same figure for swap as for memory leaves the container none.
|
|
497
|
+
"--memory",
|
|
498
|
+
memory,
|
|
499
|
+
"--memory-swap",
|
|
500
|
+
memory,
|
|
501
|
+
"--cpus",
|
|
502
|
+
String(cpus),
|
|
503
|
+
"--pids-limit",
|
|
504
|
+
String(pids)
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
function mountPath(path) {
|
|
508
|
+
if (/[",]/.test(path)) throw new Error(`${path} holds a comma or a quote, which a Docker mount cannot carry; move the runner's config file`);
|
|
509
|
+
return path;
|
|
510
|
+
}
|
|
511
|
+
function containerRunArgs(input) {
|
|
512
|
+
return [
|
|
513
|
+
"--detach",
|
|
514
|
+
"--name",
|
|
515
|
+
input.container,
|
|
516
|
+
...hardened(input),
|
|
517
|
+
"--mount",
|
|
518
|
+
`type=volume,src=${input.volume},dst=/build`,
|
|
519
|
+
// `/tmp` is a directory of the same volume, so it is measured and limited with the rest of the build.
|
|
520
|
+
"--mount",
|
|
521
|
+
`type=volume,src=${input.volume},dst=/tmp,volume-subpath=tmp`,
|
|
522
|
+
"--mount",
|
|
523
|
+
`type=bind,src=${mountPath(input.buildDir)},dst=${JOB_MOUNT},readonly`,
|
|
524
|
+
"--entrypoint",
|
|
525
|
+
"/opt/outerlayer/bin/tini",
|
|
526
|
+
input.image,
|
|
527
|
+
"--",
|
|
528
|
+
"/opt/outerlayer/bin/node",
|
|
529
|
+
"/opt/outerlayer/cli/dist/index.js",
|
|
530
|
+
"broker-relay",
|
|
531
|
+
"--port",
|
|
532
|
+
String(RELAY_PORT),
|
|
533
|
+
"--socket",
|
|
534
|
+
`${JOB_MOUNT}/tunnel.sock`,
|
|
535
|
+
...Array.from({ length: input.destinations ?? 0 }, (_unused, index) => [
|
|
536
|
+
"--forward",
|
|
537
|
+
`${DESTINATION_BASE_PORT + index}=${JOB_MOUNT}/dest-${index}.sock`
|
|
538
|
+
]).flat(),
|
|
539
|
+
...[
|
|
540
|
+
"--forward",
|
|
541
|
+
`${GATEWAY_PORT}=${JOB_MOUNT}/${GATEWAY_SOCKET}`,
|
|
542
|
+
"--forward",
|
|
543
|
+
`${CLAUDE_PORT}=${JOB_MOUNT}/${CLAUDE_SOCKET}`
|
|
544
|
+
]
|
|
545
|
+
];
|
|
546
|
+
}
|
|
547
|
+
function prepareRunArgs(input) {
|
|
548
|
+
return [
|
|
549
|
+
"--rm",
|
|
550
|
+
...hardened(input),
|
|
551
|
+
"--mount",
|
|
552
|
+
`type=volume,src=${input.volume},dst=/build`,
|
|
553
|
+
"--entrypoint",
|
|
554
|
+
"mkdir",
|
|
555
|
+
input.image,
|
|
556
|
+
"-p",
|
|
557
|
+
"-m",
|
|
558
|
+
"1777",
|
|
559
|
+
"/build/tmp",
|
|
560
|
+
"/build/home",
|
|
561
|
+
"/build/work"
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
function containerExecArgs(input) {
|
|
565
|
+
const literals = Object.keys(input.env.literals).sort().flatMap((name) => ["-e", `${name}=${input.env.literals[name]}`]);
|
|
566
|
+
const passed = Object.keys(input.env.passed).filter((name) => input.env.passed[name] !== void 0).sort().flatMap((name) => ["-e", name]);
|
|
567
|
+
return [
|
|
568
|
+
"-u",
|
|
569
|
+
input.user,
|
|
570
|
+
...input.workdir === void 0 ? [] : ["-w", input.workdir],
|
|
571
|
+
...literals,
|
|
572
|
+
...passed,
|
|
573
|
+
input.container,
|
|
574
|
+
...input.argv
|
|
575
|
+
];
|
|
576
|
+
}
|
|
359
577
|
var AssetError = class extends Error {
|
|
360
578
|
constructor(file, message) {
|
|
361
579
|
super(message);
|
|
@@ -496,7 +714,7 @@ function converterArgs(dest, extra, script) {
|
|
|
496
714
|
}
|
|
497
715
|
var firstLine2 = (text) => text.trim().split("\n")[0] ?? "";
|
|
498
716
|
function exportInto(container, converter) {
|
|
499
|
-
return new Promise((
|
|
717
|
+
return new Promise((resolve2, reject) => {
|
|
500
718
|
const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
|
|
501
719
|
const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
|
|
502
720
|
const errors = [];
|
|
@@ -513,7 +731,7 @@ function exportInto(container, converter) {
|
|
|
513
731
|
if (code !== 0 && failure === void 0) failure = `${name} exited ${code}`;
|
|
514
732
|
exits += 1;
|
|
515
733
|
if (exits < 2) return;
|
|
516
|
-
if (failure === void 0)
|
|
734
|
+
if (failure === void 0) resolve2();
|
|
517
735
|
else reject(new Error(`${failure}: ${firstLine2(errors.join("\n"))}`));
|
|
518
736
|
};
|
|
519
737
|
exporter.once("exit", (code) => done("docker export", code));
|
|
@@ -634,7 +852,9 @@ function machineConfig(limits2, paths) {
|
|
|
634
852
|
};
|
|
635
853
|
}
|
|
636
854
|
function firecrackerCommand(input) {
|
|
637
|
-
|
|
855
|
+
const args = ["--no-api", "--config-file", input.configPath];
|
|
856
|
+
if (input.cgroup === void 0) return { command: input.binary, args };
|
|
857
|
+
return { command: "sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, input.binary, ...args] };
|
|
638
858
|
}
|
|
639
859
|
var STOP_WAIT_MS = 1e4;
|
|
640
860
|
var POLL_MS = 20;
|
|
@@ -660,7 +880,7 @@ function createFirecrackerHost(options = {}) {
|
|
|
660
880
|
const failed = new Promise((_resolve, reject) => child.once("error", reject));
|
|
661
881
|
const pid = child.pid;
|
|
662
882
|
if (pid === void 0) return await failed;
|
|
663
|
-
exits.set(pid, new Promise((
|
|
883
|
+
exits.set(pid, new Promise((resolve2) => child.once("exit", () => resolve2())));
|
|
664
884
|
child.unref();
|
|
665
885
|
const start = startTimeOf(pid);
|
|
666
886
|
if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
|
|
@@ -677,8 +897,8 @@ function createFirecrackerHost(options = {}) {
|
|
|
677
897
|
}
|
|
678
898
|
const exited = exits.get(vm.pid);
|
|
679
899
|
const deadline = Date.now() + STOP_WAIT_MS;
|
|
680
|
-
if (exited !== void 0) await Promise.race([exited, new Promise((
|
|
681
|
-
while (alive(vm) && Date.now() < deadline) await new Promise((
|
|
900
|
+
if (exited !== void 0) await Promise.race([exited, new Promise((resolve2) => setTimeout(resolve2, STOP_WAIT_MS))]);
|
|
901
|
+
while (alive(vm) && Date.now() < deadline) await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
|
|
682
902
|
exits.delete(vm.pid);
|
|
683
903
|
},
|
|
684
904
|
alive
|
|
@@ -688,9 +908,10 @@ function createFirecrackerHost(options = {}) {
|
|
|
688
908
|
// src/runner/microvm/host.ts
|
|
689
909
|
function createMicroVmPorts(over = {}) {
|
|
690
910
|
return {
|
|
691
|
-
assets: over.assets ?? ((
|
|
911
|
+
assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
|
|
692
912
|
rootfs: over.rootfs ?? createRootfsHost(),
|
|
693
913
|
firecracker: over.firecracker ?? createFirecrackerHost(),
|
|
914
|
+
cgroup: over.cgroup ?? realCgroupPort(),
|
|
694
915
|
exec: over.exec ?? execCapture
|
|
695
916
|
};
|
|
696
917
|
}
|
|
@@ -845,104 +1066,6 @@ function removeStagedContext(buildDir2) {
|
|
|
845
1066
|
rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
|
|
846
1067
|
}
|
|
847
1068
|
|
|
848
|
-
// src/runner/container/run-args.ts
|
|
849
|
-
var BUILD_LABEL = "ai.outerlayer.build";
|
|
850
|
-
function hardened(input) {
|
|
851
|
-
const { memory, cpus, pids } = input.limits;
|
|
852
|
-
return [
|
|
853
|
-
"--label",
|
|
854
|
-
`${BUILD_LABEL}=${input.buildId}`,
|
|
855
|
-
"--network",
|
|
856
|
-
"none",
|
|
857
|
-
"--cap-drop",
|
|
858
|
-
"ALL",
|
|
859
|
-
"--security-opt",
|
|
860
|
-
"no-new-privileges",
|
|
861
|
-
"--read-only",
|
|
862
|
-
"--user",
|
|
863
|
-
input.user,
|
|
864
|
-
// The same figure for swap as for memory leaves the container none.
|
|
865
|
-
"--memory",
|
|
866
|
-
memory,
|
|
867
|
-
"--memory-swap",
|
|
868
|
-
memory,
|
|
869
|
-
"--cpus",
|
|
870
|
-
String(cpus),
|
|
871
|
-
"--pids-limit",
|
|
872
|
-
String(pids)
|
|
873
|
-
];
|
|
874
|
-
}
|
|
875
|
-
function mountPath(path) {
|
|
876
|
-
if (/[",]/.test(path)) throw new Error(`${path} holds a comma or a quote, which a Docker mount cannot carry; move the runner's config file`);
|
|
877
|
-
return path;
|
|
878
|
-
}
|
|
879
|
-
function containerRunArgs(input) {
|
|
880
|
-
return [
|
|
881
|
-
"--detach",
|
|
882
|
-
"--name",
|
|
883
|
-
input.container,
|
|
884
|
-
...hardened(input),
|
|
885
|
-
"--mount",
|
|
886
|
-
`type=volume,src=${input.volume},dst=/build`,
|
|
887
|
-
// `/tmp` is a directory of the same volume, so it is measured and limited with the rest of the build.
|
|
888
|
-
"--mount",
|
|
889
|
-
`type=volume,src=${input.volume},dst=/tmp,volume-subpath=tmp`,
|
|
890
|
-
"--mount",
|
|
891
|
-
`type=bind,src=${mountPath(input.buildDir)},dst=${JOB_MOUNT},readonly`,
|
|
892
|
-
"--entrypoint",
|
|
893
|
-
"/opt/outerlayer/bin/tini",
|
|
894
|
-
input.image,
|
|
895
|
-
"--",
|
|
896
|
-
"/opt/outerlayer/bin/node",
|
|
897
|
-
"/opt/outerlayer/cli/dist/index.js",
|
|
898
|
-
"broker-relay",
|
|
899
|
-
"--port",
|
|
900
|
-
String(RELAY_PORT),
|
|
901
|
-
"--socket",
|
|
902
|
-
`${JOB_MOUNT}/tunnel.sock`,
|
|
903
|
-
...Array.from({ length: input.destinations ?? 0 }, (_unused, index) => [
|
|
904
|
-
"--forward",
|
|
905
|
-
`${DESTINATION_BASE_PORT + index}=${JOB_MOUNT}/dest-${index}.sock`
|
|
906
|
-
]).flat(),
|
|
907
|
-
...[
|
|
908
|
-
"--forward",
|
|
909
|
-
`${GATEWAY_PORT}=${JOB_MOUNT}/${GATEWAY_SOCKET}`,
|
|
910
|
-
"--forward",
|
|
911
|
-
`${CLAUDE_PORT}=${JOB_MOUNT}/${CLAUDE_SOCKET}`
|
|
912
|
-
]
|
|
913
|
-
];
|
|
914
|
-
}
|
|
915
|
-
function prepareRunArgs(input) {
|
|
916
|
-
return [
|
|
917
|
-
"--rm",
|
|
918
|
-
...hardened(input),
|
|
919
|
-
"--mount",
|
|
920
|
-
`type=volume,src=${input.volume},dst=/build`,
|
|
921
|
-
"--entrypoint",
|
|
922
|
-
"mkdir",
|
|
923
|
-
input.image,
|
|
924
|
-
"-p",
|
|
925
|
-
"-m",
|
|
926
|
-
"1777",
|
|
927
|
-
"/build/tmp",
|
|
928
|
-
"/build/home",
|
|
929
|
-
"/build/work"
|
|
930
|
-
];
|
|
931
|
-
}
|
|
932
|
-
function containerExecArgs(input) {
|
|
933
|
-
const literals = Object.keys(input.env.literals).sort().flatMap((name) => ["-e", `${name}=${input.env.literals[name]}`]);
|
|
934
|
-
const passed = Object.keys(input.env.passed).filter((name) => input.env.passed[name] !== void 0).sort().flatMap((name) => ["-e", name]);
|
|
935
|
-
return [
|
|
936
|
-
"-u",
|
|
937
|
-
input.user,
|
|
938
|
-
...input.workdir === void 0 ? [] : ["-w", input.workdir],
|
|
939
|
-
...literals,
|
|
940
|
-
...passed,
|
|
941
|
-
input.container,
|
|
942
|
-
...input.argv
|
|
943
|
-
];
|
|
944
|
-
}
|
|
945
|
-
|
|
946
1069
|
// src/runner/container/docker.ts
|
|
947
1070
|
function createContainerHost(deps = {}) {
|
|
948
1071
|
const run = deps.run ?? spawnRun;
|
|
@@ -1027,9 +1150,9 @@ async function git(args, cwd, env, input) {
|
|
|
1027
1150
|
if (size < maxOutputBytes) out.push(chunk);
|
|
1028
1151
|
size += chunk.length;
|
|
1029
1152
|
});
|
|
1030
|
-
const closed = new Promise((
|
|
1153
|
+
const closed = new Promise((resolve2, reject) => {
|
|
1031
1154
|
child.once("error", reject);
|
|
1032
|
-
child.once("close", (status) =>
|
|
1155
|
+
child.once("close", (status) => resolve2(status));
|
|
1033
1156
|
});
|
|
1034
1157
|
if (input !== void 0) {
|
|
1035
1158
|
await pipeline(input, child.stdin).catch(() => void 0);
|
|
@@ -1078,7 +1201,7 @@ function decode(prefix, gzip) {
|
|
|
1078
1201
|
if (!gzip) return prefix;
|
|
1079
1202
|
try {
|
|
1080
1203
|
const { compressedWindow, inflatedBytes } = limits();
|
|
1081
|
-
return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
|
|
1204
|
+
return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants$1.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
|
|
1082
1205
|
} catch (err) {
|
|
1083
1206
|
const code = err.code;
|
|
1084
1207
|
if (code === "ERR_BUFFER_TOO_LARGE") return refused(TOO_LONG);
|
|
@@ -1226,14 +1349,14 @@ async function drain(req) {
|
|
|
1226
1349
|
}
|
|
1227
1350
|
}
|
|
1228
1351
|
function readPrefix(req, gzip) {
|
|
1229
|
-
return new Promise((
|
|
1352
|
+
return new Promise((resolve2, reject) => {
|
|
1230
1353
|
const held = [];
|
|
1231
1354
|
const finish = (section) => {
|
|
1232
1355
|
req.off("data", onData);
|
|
1233
1356
|
req.off("end", onEnd);
|
|
1234
1357
|
req.off("error", reject);
|
|
1235
1358
|
req.pause();
|
|
1236
|
-
|
|
1359
|
+
resolve2({ held, section });
|
|
1237
1360
|
};
|
|
1238
1361
|
const onData = (chunk) => {
|
|
1239
1362
|
held.push(chunk);
|
|
@@ -1247,19 +1370,19 @@ function readPrefix(req, gzip) {
|
|
|
1247
1370
|
});
|
|
1248
1371
|
}
|
|
1249
1372
|
function readBody(req, limit) {
|
|
1250
|
-
return new Promise((
|
|
1373
|
+
return new Promise((resolve2, reject) => {
|
|
1251
1374
|
const chunks = [];
|
|
1252
1375
|
let size = 0;
|
|
1253
1376
|
req.on("data", (chunk) => {
|
|
1254
1377
|
size += chunk.length;
|
|
1255
1378
|
if (size > limit) {
|
|
1256
1379
|
req.pause();
|
|
1257
|
-
|
|
1380
|
+
resolve2(void 0);
|
|
1258
1381
|
return;
|
|
1259
1382
|
}
|
|
1260
1383
|
chunks.push(chunk);
|
|
1261
1384
|
});
|
|
1262
|
-
req.once("end", () =>
|
|
1385
|
+
req.once("end", () => resolve2(Buffer.concat(chunks)));
|
|
1263
1386
|
req.once("error", reject);
|
|
1264
1387
|
});
|
|
1265
1388
|
}
|
|
@@ -1272,7 +1395,7 @@ function createGitRemote(options) {
|
|
|
1272
1395
|
const { route, res } = input;
|
|
1273
1396
|
const url = new URL(`${gitUrl}/${route.owner}/${route.name}.git/${route.kind === "info-refs" ? "info/refs" : route.kind === "lfs-batch" ? "info/lfs/objects/batch" : `git-${route.kind}`}${route.search === "" ? "" : `?${route.search}`}`);
|
|
1274
1397
|
const send = url.protocol === "https:" ? request$1 : request;
|
|
1275
|
-
return new Promise((
|
|
1398
|
+
return new Promise((resolve2) => {
|
|
1276
1399
|
const upstream = send(url, { method: input.method, headers: { ...input.headers, connection: "close" } }, (answer) => {
|
|
1277
1400
|
const headers = {};
|
|
1278
1401
|
for (const [name, value] of Object.entries(answer.headers)) {
|
|
@@ -1294,18 +1417,18 @@ function createGitRemote(options) {
|
|
|
1294
1417
|
});
|
|
1295
1418
|
answer.once("end", () => {
|
|
1296
1419
|
res.end();
|
|
1297
|
-
|
|
1420
|
+
resolve2();
|
|
1298
1421
|
});
|
|
1299
1422
|
answer.once("error", () => {
|
|
1300
1423
|
res.destroy();
|
|
1301
|
-
|
|
1424
|
+
resolve2();
|
|
1302
1425
|
});
|
|
1303
1426
|
});
|
|
1304
1427
|
upstream.on("error", (err) => {
|
|
1305
1428
|
log(`could not reach GitHub for ${route.kind}: ${err.message}`);
|
|
1306
1429
|
if (res.headersSent) res.destroy();
|
|
1307
1430
|
else plain(res, 502, "the runner could not reach GitHub\n");
|
|
1308
|
-
|
|
1431
|
+
resolve2();
|
|
1309
1432
|
});
|
|
1310
1433
|
res.once("close", () => upstream.destroy());
|
|
1311
1434
|
input.feed(upstream);
|
|
@@ -1594,7 +1717,7 @@ async function startTunnel(socketPath, opts = {}) {
|
|
|
1594
1717
|
if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
|
|
1595
1718
|
throw new Error(`${socketPath} is too long for a Unix socket; keep the runner's config file nearer the root of the filesystem`);
|
|
1596
1719
|
}
|
|
1597
|
-
const
|
|
1720
|
+
const resolve2 = opts.resolve ?? systemResolve;
|
|
1598
1721
|
const openConnection = opts.connect ?? ((t) => connect({ host: t.address, port: t.port }));
|
|
1599
1722
|
const ownAddresses = opts.hostAddresses ?? systemHostAddresses;
|
|
1600
1723
|
const connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
|
|
@@ -1678,7 +1801,7 @@ async function startTunnel(socketPath, opts = {}) {
|
|
|
1678
1801
|
}
|
|
1679
1802
|
let addresses;
|
|
1680
1803
|
try {
|
|
1681
|
-
addresses = await
|
|
1804
|
+
addresses = await resolve2(host);
|
|
1682
1805
|
} catch {
|
|
1683
1806
|
return { ok: false, status: 502, body: `could not resolve ${host}
|
|
1684
1807
|
` };
|
|
@@ -1863,7 +1986,7 @@ function createContainerRuntime(opts = {}) {
|
|
|
1863
1986
|
)),
|
|
1864
1987
|
startTunnel: opts.startTunnel ?? startTunnel,
|
|
1865
1988
|
tunnelOptions: opts.tunnelOptions,
|
|
1866
|
-
sleep: opts.sleep ?? ((ms) => new Promise((
|
|
1989
|
+
sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
|
|
1867
1990
|
hostEnv: opts.hostEnv ?? process.env,
|
|
1868
1991
|
credentials: opts.credentials ?? /* @__PURE__ */ new Map(),
|
|
1869
1992
|
github: { gitUrl: opts.github?.gitUrl ?? "https://github.com", apiUrl: opts.github?.apiUrl ?? "https://api.github.com" },
|
|
@@ -2035,11 +2158,13 @@ async function provisionContainer(input) {
|
|
|
2035
2158
|
return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
|
|
2036
2159
|
}
|
|
2037
2160
|
const limits2 = { memory: config.build?.memory ?? "8g", cpus: config.build?.cpus ?? 4, pids: config.build?.pids ?? 4096 };
|
|
2038
|
-
const
|
|
2161
|
+
const runnerId = runnerLabelValue(config.configPath);
|
|
2162
|
+
const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
|
|
2039
2163
|
if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
|
|
2040
2164
|
if (input.aborted()) return bail("", true);
|
|
2041
2165
|
const runInput = {
|
|
2042
2166
|
buildId,
|
|
2167
|
+
runnerId,
|
|
2043
2168
|
container: name,
|
|
2044
2169
|
volume: name,
|
|
2045
2170
|
buildDir: buildDir2,
|
|
@@ -2132,6 +2257,40 @@ function containerCommandLine(input) {
|
|
|
2132
2257
|
env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
|
|
2133
2258
|
};
|
|
2134
2259
|
}
|
|
2260
|
+
function containerSenderCommand(input) {
|
|
2261
|
+
const state = input.job.container;
|
|
2262
|
+
const env = buildEnvironment({
|
|
2263
|
+
config: input.config,
|
|
2264
|
+
item: input.job.item,
|
|
2265
|
+
itemKey: input.job.itemKey,
|
|
2266
|
+
branch: input.job.branch,
|
|
2267
|
+
repository: state.repository,
|
|
2268
|
+
hostEnv: input.runtime.hostEnv,
|
|
2269
|
+
containerEnv: state.containerEnv ?? {},
|
|
2270
|
+
builtIns: state.builtIns === true,
|
|
2271
|
+
identity: input.job.commitIdentity
|
|
2272
|
+
});
|
|
2273
|
+
const args = containerExecArgs({
|
|
2274
|
+
container: state.container,
|
|
2275
|
+
user: state.agentUser ?? ADDED_USER,
|
|
2276
|
+
workdir: CHECKOUT_DIR,
|
|
2277
|
+
env,
|
|
2278
|
+
argv: ["outerlayer", "session-sender"]
|
|
2279
|
+
});
|
|
2280
|
+
return { command: "docker", args: ["exec", ...args], env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed } };
|
|
2281
|
+
}
|
|
2282
|
+
function senderSignalScript(pidfile, signal) {
|
|
2283
|
+
const file = shellQuote(pidfile);
|
|
2284
|
+
return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && kill -${signal} "$p"`;
|
|
2285
|
+
}
|
|
2286
|
+
async function signalInContainer(runtime, state, pidfile, signal) {
|
|
2287
|
+
await runtime.host.exec({
|
|
2288
|
+
container: state.container,
|
|
2289
|
+
user: state.agentUser ?? ADDED_USER,
|
|
2290
|
+
env: NO_ENV,
|
|
2291
|
+
argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)]
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2135
2294
|
async function stopContainer(runtime, state) {
|
|
2136
2295
|
await runtime.host.stop(state.container);
|
|
2137
2296
|
state.stopped = true;
|
|
@@ -2230,4 +2389,4 @@ async function cleanupContainer(runtime, state, jobDir) {
|
|
|
2230
2389
|
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
2231
2390
|
}
|
|
2232
2391
|
|
|
2233
|
-
export { ADDED_USER, AgentUserError, AssetError, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, classifyRefusal, cleanupContainer, containerCommandLine, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, shellQuote, shortClaim, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
|
|
2392
|
+
export { ADDED_USER, AgentUserError, AssetError, BUILD_LABEL, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, RUNNER_LABEL, classifyRefusal, cleanupContainer, containerCommandLine, containerSenderCommand, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, runnerLabelValue, senderSignalScript, setUpRunnerCgroup, shellQuote, shortClaim, signalInContainer, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
createRequire(import.meta.url);
|
|
8
|
+
function readJobFile(path) {
|
|
9
|
+
if (!existsSync(path)) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readJobFileChecked(path) {
|
|
17
|
+
if (!existsSync(path)) return { kind: "missing" };
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { kind: "unreadable" };
|
|
21
|
+
return { kind: "ok", job: parsed };
|
|
22
|
+
} catch {
|
|
23
|
+
return { kind: "unreadable" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeJobFile(path, job) {
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
28
|
+
writeFileSync(path, JSON.stringify(job, null, 2), { mode: RUNNER_FILE_MODE });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { readJobFile, readJobFileChecked, writeJobFile };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
4
|
+
import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
createRequire(import.meta.url);
|
|
8
|
+
var FILE = "housekeeping.json";
|
|
9
|
+
function readHousekeepingRecord(runnerDirPath) {
|
|
10
|
+
const path = join(runnerDirPath, FILE);
|
|
11
|
+
try {
|
|
12
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
const record = {};
|
|
14
|
+
if (typeof raw.lastRunAt === "string") record.lastRunAt = raw.lastRunAt;
|
|
15
|
+
if (typeof raw.freedBytes === "number") record.freedBytes = raw.freedBytes;
|
|
16
|
+
const holding = raw.holding;
|
|
17
|
+
if (typeof holding?.free === "number" && typeof holding.needed === "number" && typeof holding.since === "string") {
|
|
18
|
+
record.holding = { free: holding.free, needed: holding.needed, since: holding.since };
|
|
19
|
+
}
|
|
20
|
+
return record;
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeHousekeepingRecord(runnerDirPath, patch) {
|
|
26
|
+
const merged = { ...readHousekeepingRecord(runnerDirPath), ...patch };
|
|
27
|
+
mkdirSync(runnerDirPath, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
28
|
+
const path = join(runnerDirPath, FILE);
|
|
29
|
+
const temp = `${path}.partial-${process.pid}`;
|
|
30
|
+
writeFileSync(temp, JSON.stringify(merged, null, 2), { mode: RUNNER_FILE_MODE });
|
|
31
|
+
renameSync(temp, path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { readHousekeepingRecord, writeHousekeepingRecord };
|