@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
|
@@ -1,30 +1,78 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { LEASE_SECONDS, releaseLease, isUnfinished, writeJobFile, setStage, jobEnv, runHook, describeHookEnd, provisionMicroVm, deliverCommitHooks, provisionProcess, provisionEnvironment, readProvisionOut, resolveHostBuild, workdirProblem, listenOnBroker, hostEnvironment, hostCommandEnvironment, execArgv, microVmCommandLine, runCommand, execCommandLine, startHostServices, finishJob, isGroupAlive, outcomeFromExitCode, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, isTimedOut, readCpuUsec, isIdle, stopOrphanedMicroVms, unfinishedJobs, listAdoptableJobs, retryFailedReleases, recoverLostJobs, readJobFile, hookLimitsFor } from './chunk-RWOTHXYE.js';
|
|
4
|
-
import { RUNNER_PROTOCOL } from './chunk-QDQEUVUF.js';
|
|
5
3
|
import { fetchVersionMeta, npmVersion, npmSupportsProvenance, fetchTarballDigest, fetchProvenance, installVersion, auditSignatures } from './chunk-ABIWDUMS.js';
|
|
6
4
|
import { readMarker, currentVersion, removeMarker, writeMarker, entryIn, versionDir, switchCurrent, cleanVersions, isInLayout, startedThroughCurrent, stagingDir } from './chunk-YYYXRJUW.js';
|
|
5
|
+
import { LEASE_SECONDS, releaseLease, isUnfinished, setStage, jobEnv, runHook, describeHookEnd, provisionMicroVm, deliverCommitHooks, provisionProcess, provisionEnvironment, readProvisionOut, resolveHostBuild, workdirProblem, listenOnBroker, hostEnvironment, hostCommandEnvironment, execArgv, microVmCommandLine, runCommand, execCommandLine, startSender, startHostServices, finishJob, isGroupAlive, outcomeFromExitCode, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, isTimedOut, readCpuUsec, isIdle, stopOrphanedMicroVms, unfinishedJobs, listAdoptableJobs, retryFailedReleases, recoverLostJobs, hookLimitsFor, ALREADY_RELEASED } from './chunk-VMJ3MMBX.js';
|
|
6
|
+
import { writeJobFile, readJobFile, readJobFileChecked } from './chunk-T6OIJQRT.js';
|
|
7
|
+
import { RUNNER_PROTOCOL } from './chunk-AIW6GLET.js';
|
|
8
|
+
import { readHousekeepingRecord, writeHousekeepingRecord } from './chunk-UGP76IZ5.js';
|
|
9
|
+
import { placeholderQueues, placeholderProblem } from './chunk-MXB5VB4Y.js';
|
|
10
|
+
import { freeBytes, formatBytes } from './chunk-YYYRBYWG.js';
|
|
7
11
|
import { RepoFilter } from './chunk-TBT347UY.js';
|
|
8
|
-
import {
|
|
12
|
+
import { checkFit } from './chunk-OUUINEYB.js';
|
|
13
|
+
import { describeCallFailure, retrySend, shortClaim, relativeToRunner, formatDuration, provisionContainer, lifecycleSteps, lifecycleFailureReason, prepareBranch, containerCommandLine, BuildTokens, classifyRefusal, refusalCode, formatClock, measureDisk, createRunnerLog, setUpRunnerCgroup, ensureTunnel, keepGhConfigCurrent, createContainerRuntime, runnerLabelValue, BUILD_LABEL, RUNNER_LABEL } from './chunk-RZSF5B46.js';
|
|
9
14
|
import { startTimeOf, claimPidFile, readPidFile, isSameProcess, releasePidFile } from './chunk-N5FOE5PS.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
15
|
+
import { spawnRun } from './chunk-774HEQL6.js';
|
|
16
|
+
import { isSignatureRefusal, WorkCommandError, getJson, postJson } from './chunk-BNMCXPG7.js';
|
|
17
|
+
import { isBuiltinContainer, isBuiltinVm, isBuiltinProcess, loadRunnerConfig, checkHooks, builtinKind } from './chunk-3DD6AOWC.js';
|
|
12
18
|
import { exceedsDisk } from './chunk-F6GFZXZ3.js';
|
|
13
|
-
import { parseClaimIdentity, sharedUserEnvironment, JOB_MOUNT } from './chunk-AUFG23AA.js';
|
|
19
|
+
import { parseClaimIdentity, sharedUserEnvironment, JOB_MOUNT, claudeCredential } from './chunk-AUFG23AA.js';
|
|
14
20
|
import { resolveVersion } from './chunk-RA3O54FC.js';
|
|
15
21
|
import { loadHostKey, ensureHostKey, signerFor } from './chunk-M5POMKKW.js';
|
|
16
|
-
import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, jobsDir, pidFilePath, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
22
|
+
import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, jobsDir, pidFilePath, spoolDir, RUNNER_FILE_MODE, rootfsDir } from './chunk-ZM2IMMYN.js';
|
|
17
23
|
import { HOOKS_DIR_NAME, writeCommitHooks } from './chunk-XCCLFVXM.js';
|
|
18
24
|
import { randomBytes } from 'crypto';
|
|
19
|
-
import { mkdirSync, existsSync, chmodSync, statSync, readdirSync, readFileSync, rmSync, renameSync, writeFileSync } from 'fs';
|
|
25
|
+
import { mkdirSync, existsSync, chmodSync, statSync, readdirSync, readFileSync, rmSync, renameSync, writeFileSync, closeSync, openSync, constants, lstatSync } from 'fs';
|
|
20
26
|
import { join, dirname } from 'path';
|
|
27
|
+
import { totalmem } from 'os';
|
|
21
28
|
|
|
22
29
|
createRequire(import.meta.url);
|
|
30
|
+
function openKvm() {
|
|
31
|
+
try {
|
|
32
|
+
closeSync(openSync("/dev/kvm", constants.O_RDWR));
|
|
33
|
+
return "usable";
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return err.code === "ENOENT" ? "absent" : "no-permission";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function realBlockedProbes() {
|
|
39
|
+
return { env: process.env, kvm: openKvm, totalMemoryBytes: totalmem };
|
|
40
|
+
}
|
|
41
|
+
var DEFAULT_BUILD_MEMORY = "8g";
|
|
42
|
+
function checkBlocked(config, probes) {
|
|
43
|
+
const placeholders = placeholderQueues(config);
|
|
44
|
+
if (placeholders.length > 0) return { reason: `${placeholderProblem(placeholders)}. The runner takes work again once the config is edited` };
|
|
45
|
+
const kind = builtinKind(config.hooks.provision);
|
|
46
|
+
if (kind === "container" || kind === "vm") {
|
|
47
|
+
const fit = checkFit({
|
|
48
|
+
concurrency: config.concurrency,
|
|
49
|
+
buildMemory: config.build?.memory ?? DEFAULT_BUILD_MEMORY,
|
|
50
|
+
usableBytes: probes.totalMemoryBytes()
|
|
51
|
+
});
|
|
52
|
+
if (!fit.fits) return { reason: `${fit.message}. Lower runner.concurrency or runner.build.memory` };
|
|
53
|
+
}
|
|
54
|
+
const kvm = kind === "vm" ? probes.kvm() : "usable";
|
|
55
|
+
if (kvm === "absent") {
|
|
56
|
+
return { reason: "/dev/kvm does not exist on this machine, so builtin:vm cannot start a microVM. Use a host with KVM enabled, or builtin:container hooks" };
|
|
57
|
+
}
|
|
58
|
+
if (kvm !== "usable") {
|
|
59
|
+
return {
|
|
60
|
+
reason: "the runner cannot open /dev/kvm, so builtin:vm cannot start a microVM. Add the runner's account to the kvm group, then restart the service, because a service reads its groups only when it starts"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (kind !== "own" && claudeCredential(probes.env) === void 0) {
|
|
64
|
+
return {
|
|
65
|
+
reason: "there is no Claude credential in the runner's environment. Put CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`) or ANTHROPIC_API_KEY in the runner.env beside the config, then restart the service"
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
23
70
|
|
|
24
71
|
// src/runner/heartbeat.ts
|
|
25
72
|
var HEARTBEAT_TIMEOUT_MS = 1e4;
|
|
26
73
|
var ERROR_CODE_MAX = 100;
|
|
27
74
|
var ERROR_MESSAGE_MAX = 500;
|
|
75
|
+
var BLOCKED_REASON_MAX = 500;
|
|
28
76
|
var UNREACHABLE_MESSAGE = "the request did not reach the platform";
|
|
29
77
|
function noPlatformMessage(status) {
|
|
30
78
|
return `the request got HTTP ${status} with no error message from the platform`;
|
|
@@ -68,7 +116,11 @@ async function sendHeartbeat(args, fetchImpl) {
|
|
|
68
116
|
outcome: args.lastUpdate.outcome,
|
|
69
117
|
reason: args.lastUpdate.reason,
|
|
70
118
|
...args.lastUpdate.at !== void 0 ? { at: args.lastUpdate.at } : {}
|
|
71
|
-
}
|
|
119
|
+
},
|
|
120
|
+
// Free disk on the runner's file system, in bytes. Sent only while held, so a host with room sends nothing about its disk.
|
|
121
|
+
lowDisk: args.lowDisk === void 0 ? void 0 : { free: args.lowDisk.free, needed: args.lowDisk.needed },
|
|
122
|
+
// Why the runner claims nothing. Sent only while it holds, so a runner that takes work says nothing about it.
|
|
123
|
+
blocked: args.blocked === void 0 ? void 0 : { reason: cutForGateway(args.blocked.reason, BLOCKED_REASON_MAX) }
|
|
72
124
|
};
|
|
73
125
|
try {
|
|
74
126
|
const response = await postJson(
|
|
@@ -371,9 +423,9 @@ function retryFailure(version, err) {
|
|
|
371
423
|
}
|
|
372
424
|
return { outcome: "retrying", reason: messageOf(err) };
|
|
373
425
|
}
|
|
374
|
-
function finish(args,
|
|
375
|
-
let update = { from: args.own, to: args.version, outcome:
|
|
376
|
-
if (
|
|
426
|
+
function finish(args, failure2) {
|
|
427
|
+
let update = { from: args.own, to: args.version, outcome: failure2.outcome, reason: failure2.reason, at: isoAt(args.now) };
|
|
428
|
+
if (failure2.outcome === "retrying") {
|
|
377
429
|
const previous = readLastUpdate(args.configPath);
|
|
378
430
|
const attempts = previous?.to === args.version && previous.outcome === "retrying" ? (previous.attempts ?? 1) + 1 : 1;
|
|
379
431
|
update = { ...update, attempts };
|
|
@@ -383,16 +435,16 @@ function finish(args, failure) {
|
|
|
383
435
|
} catch (err) {
|
|
384
436
|
args.log.error(`could not record the update attempt: ${messageOf(err)}`);
|
|
385
437
|
}
|
|
386
|
-
if (
|
|
438
|
+
if (failure2.outcome === "retrying") {
|
|
387
439
|
return {
|
|
388
440
|
kind: "retrying",
|
|
389
|
-
reason:
|
|
390
|
-
detail:
|
|
441
|
+
reason: failure2.reason,
|
|
442
|
+
detail: failure2.detail ?? failure2.reason,
|
|
391
443
|
attempts: update.attempts ?? 1,
|
|
392
444
|
retryAtMs: retryAfterMs(update)
|
|
393
445
|
};
|
|
394
446
|
}
|
|
395
|
-
return { kind: "failed", outcome:
|
|
447
|
+
return { kind: "failed", outcome: failure2.outcome, reason: failure2.reason };
|
|
396
448
|
}
|
|
397
449
|
async function earlyFailure(args) {
|
|
398
450
|
const current = currentVersion(args.configPath);
|
|
@@ -423,8 +475,8 @@ async function earlyFailure(args) {
|
|
|
423
475
|
return void 0;
|
|
424
476
|
}
|
|
425
477
|
async function preflightUpdate(args) {
|
|
426
|
-
const
|
|
427
|
-
return
|
|
478
|
+
const failure2 = await earlyFailure(args);
|
|
479
|
+
return failure2 === void 0 ? void 0 : finish(args, failure2);
|
|
428
480
|
}
|
|
429
481
|
async function performUpdate(args) {
|
|
430
482
|
const { configPath, own, version, deps } = args;
|
|
@@ -525,12 +577,325 @@ function settleUpdate(args) {
|
|
|
525
577
|
}
|
|
526
578
|
}
|
|
527
579
|
|
|
580
|
+
// src/runner/housekeeping/docker.ts
|
|
581
|
+
var KIND_LABEL = "ai.outerlayer.kind";
|
|
582
|
+
function failure(result, what) {
|
|
583
|
+
return new Error(`${what}: ${result.stderr.trim() || `docker exited ${result.status}`}`);
|
|
584
|
+
}
|
|
585
|
+
function labelled(stdout, runnerId) {
|
|
586
|
+
const found = [];
|
|
587
|
+
for (const line of stdout.split("\n")) {
|
|
588
|
+
const [key = "", build = "", runner = ""] = line.replace(/\r?\n$/, "").split(" ");
|
|
589
|
+
if (key.trim() === "" || build === "") continue;
|
|
590
|
+
if (runner !== "" && runner !== runnerId) continue;
|
|
591
|
+
found.push({ key: key.trim(), build, owned: runner === runnerId });
|
|
592
|
+
}
|
|
593
|
+
return found;
|
|
594
|
+
}
|
|
595
|
+
function decimalUnits() {
|
|
596
|
+
return { b: 1, kb: 1e3, mb: 1e6, gb: 1e9, tb: 1e12, pb: 1e15 };
|
|
597
|
+
}
|
|
598
|
+
function parsePrunedBytes(stdout) {
|
|
599
|
+
const match = /^Total(?: reclaimed space)?:\s*([\d.]+)\s*([A-Za-z]*)\s*$/im.exec(stdout);
|
|
600
|
+
if (match === null) return 0;
|
|
601
|
+
const unit = decimalUnits()[match[2].toLowerCase() || "b"];
|
|
602
|
+
return unit === void 0 ? 0 : Math.round(Number(match[1]) * unit);
|
|
603
|
+
}
|
|
604
|
+
function createHousekeepingDocker(deps) {
|
|
605
|
+
const run = deps.run ?? spawnRun;
|
|
606
|
+
return {
|
|
607
|
+
async listBuildContainers() {
|
|
608
|
+
const result = await run("docker", [
|
|
609
|
+
"ps",
|
|
610
|
+
"--all",
|
|
611
|
+
"--no-trunc",
|
|
612
|
+
"--filter",
|
|
613
|
+
`label=${BUILD_LABEL}`,
|
|
614
|
+
"--format",
|
|
615
|
+
`{{.ID}} {{.Label "${BUILD_LABEL}"}} {{.Label "${RUNNER_LABEL}"}}`
|
|
616
|
+
]);
|
|
617
|
+
if (result.status !== 0) throw failure(result, "docker ps");
|
|
618
|
+
return labelled(result.stdout, deps.runnerId).map(({ key, build, owned }) => ({ id: key, build, owned }));
|
|
619
|
+
},
|
|
620
|
+
async listBuildVolumes() {
|
|
621
|
+
const result = await run("docker", [
|
|
622
|
+
"volume",
|
|
623
|
+
"ls",
|
|
624
|
+
"--filter",
|
|
625
|
+
`label=${BUILD_LABEL}`,
|
|
626
|
+
"--format",
|
|
627
|
+
`{{.Name}} {{.Label "${BUILD_LABEL}"}} {{.Label "${RUNNER_LABEL}"}}`
|
|
628
|
+
]);
|
|
629
|
+
if (result.status !== 0) throw failure(result, "docker volume ls");
|
|
630
|
+
return labelled(result.stdout, deps.runnerId).map(({ key, build, owned }) => ({ name: key, build, owned }));
|
|
631
|
+
},
|
|
632
|
+
async removeContainer(id) {
|
|
633
|
+
const result = await run("docker", ["rm", "--force", "--volumes", id]);
|
|
634
|
+
if (result.status !== 0) throw failure(result, `docker rm ${id}`);
|
|
635
|
+
},
|
|
636
|
+
async removeVolume(name) {
|
|
637
|
+
const result = await run("docker", ["volume", "rm", "--force", name]);
|
|
638
|
+
if (result.status !== 0) throw failure(result, `docker volume rm ${name}`);
|
|
639
|
+
},
|
|
640
|
+
async imageState(ref) {
|
|
641
|
+
const result = await run("docker", ["image", "inspect", "--format", "{{.Id}}", ref]);
|
|
642
|
+
if (result.status === 0) return "present";
|
|
643
|
+
return /no such image|not found/i.test(result.stderr) ? "missing" : "unknown";
|
|
644
|
+
},
|
|
645
|
+
async listDanglingImages() {
|
|
646
|
+
const listed = await run("docker", [
|
|
647
|
+
"image",
|
|
648
|
+
"ls",
|
|
649
|
+
"--all",
|
|
650
|
+
"--no-trunc",
|
|
651
|
+
"--quiet",
|
|
652
|
+
"--filter",
|
|
653
|
+
"dangling=true",
|
|
654
|
+
"--filter",
|
|
655
|
+
`label=${KIND_LABEL}`
|
|
656
|
+
]);
|
|
657
|
+
if (listed.status !== 0) throw failure(listed, "docker image ls");
|
|
658
|
+
const ids = [...new Set(listed.stdout.split("\n").map((l) => l.trim()).filter(Boolean))];
|
|
659
|
+
if (ids.length === 0) return [];
|
|
660
|
+
const inspected = await run("docker", ["image", "inspect", ...ids]);
|
|
661
|
+
if (inspected.status !== 0) throw failure(inspected, "docker image inspect");
|
|
662
|
+
return JSON.parse(inspected.stdout).map((row) => ({
|
|
663
|
+
id: row.Id,
|
|
664
|
+
size: row.Size ?? 0,
|
|
665
|
+
labels: row.Config?.Labels ?? {}
|
|
666
|
+
}));
|
|
667
|
+
},
|
|
668
|
+
async removeImage(id) {
|
|
669
|
+
const result = await run("docker", ["rmi", id]);
|
|
670
|
+
if (result.status !== 0) throw failure(result, `docker rmi ${id}`);
|
|
671
|
+
},
|
|
672
|
+
async builderPrune(keepBytes) {
|
|
673
|
+
const result = await run("docker", ["builder", "prune", "--force", "--keep-storage", String(keepBytes)]);
|
|
674
|
+
if (result.status !== 0) throw failure(result, "docker builder prune");
|
|
675
|
+
return { freedBytes: parsePrunedBytes(result.stdout) };
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/runner/housekeeping/select.ts
|
|
681
|
+
var DAY_MS = 864e5;
|
|
682
|
+
function spoolMaxAgeMs() {
|
|
683
|
+
return 24 * 36e5;
|
|
684
|
+
}
|
|
685
|
+
function selectJobDirs(entries, nowMs, keepDays) {
|
|
686
|
+
return entries.filter(
|
|
687
|
+
(entry) => (entry.state === "finished" || entry.state === "no_job_file") && nowMs - entry.mtimeMs > keepDays * DAY_MS
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function liveBuildIds(jobs) {
|
|
691
|
+
const ids = /* @__PURE__ */ new Set();
|
|
692
|
+
for (const job of jobs) {
|
|
693
|
+
for (const id of [job.container?.buildId, job.microvm?.buildId, job.build?.buildId]) if (id !== void 0) ids.add(id);
|
|
694
|
+
}
|
|
695
|
+
return ids;
|
|
696
|
+
}
|
|
697
|
+
function finishedBuildIds(jobs) {
|
|
698
|
+
return liveBuildIds(jobs);
|
|
699
|
+
}
|
|
700
|
+
function liveImageIds(jobs) {
|
|
701
|
+
const ids = /* @__PURE__ */ new Set();
|
|
702
|
+
for (const job of jobs) {
|
|
703
|
+
for (const id of [job.container?.imageId, job.microvm?.imageId]) if (id !== void 0) ids.add(id);
|
|
704
|
+
}
|
|
705
|
+
return ids;
|
|
706
|
+
}
|
|
707
|
+
function selectOrphans(items, live, finished) {
|
|
708
|
+
return items.filter((item) => !live.has(item.build) && (item.owned || finished.has(item.build)));
|
|
709
|
+
}
|
|
710
|
+
function rootDiskImageId(fileName) {
|
|
711
|
+
if (!fileName.endsWith(".ext4") || fileName.includes(".partial-")) return void 0;
|
|
712
|
+
const id = fileName.slice(0, -".ext4".length);
|
|
713
|
+
return id === "" ? void 0 : id;
|
|
714
|
+
}
|
|
715
|
+
function selectSpool(files, nowMs, buildRunning) {
|
|
716
|
+
return buildRunning ? [] : files.filter((file) => nowMs - file.mtimeMs > spoolMaxAgeMs());
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/runner/housekeeping/run.ts
|
|
720
|
+
function sizeOf(path) {
|
|
721
|
+
try {
|
|
722
|
+
const stats = lstatSync(path);
|
|
723
|
+
if (!stats.isDirectory()) return stats.size;
|
|
724
|
+
return readdirSync(path).reduce((sum, name) => sum + sizeOf(join(path, name)), 0);
|
|
725
|
+
} catch {
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function listDir(path) {
|
|
730
|
+
try {
|
|
731
|
+
return readdirSync(path);
|
|
732
|
+
} catch {
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function mtimeOf(path) {
|
|
737
|
+
try {
|
|
738
|
+
return statSync(path).mtimeMs;
|
|
739
|
+
} catch {
|
|
740
|
+
return void 0;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function readJobs(jobsPath) {
|
|
744
|
+
const entries = [];
|
|
745
|
+
const unfinished = [];
|
|
746
|
+
const finished = [];
|
|
747
|
+
const unreadable = [];
|
|
748
|
+
for (const dirent of existsSync(jobsPath) ? readdirSync(jobsPath, { withFileTypes: true }) : []) {
|
|
749
|
+
if (!dirent.isDirectory()) continue;
|
|
750
|
+
const dirPath = join(jobsPath, dirent.name);
|
|
751
|
+
const mtimeMs = mtimeOf(dirPath);
|
|
752
|
+
if (mtimeMs === void 0) continue;
|
|
753
|
+
const read = readJobFileChecked(join(dirPath, "job.json"));
|
|
754
|
+
let state;
|
|
755
|
+
if (read.kind === "missing") {
|
|
756
|
+
state = "no_job_file";
|
|
757
|
+
} else if (read.kind === "unreadable") {
|
|
758
|
+
unreadable.push(dirent.name);
|
|
759
|
+
state = "unreadable";
|
|
760
|
+
} else if (isUnfinished(read.job)) {
|
|
761
|
+
unfinished.push(read.job);
|
|
762
|
+
state = "unfinished";
|
|
763
|
+
} else {
|
|
764
|
+
finished.push(read.job);
|
|
765
|
+
state = read.job.releaseFailed === void 0 || read.job.releaseFailed === ALREADY_RELEASED ? "finished" : "release_failed";
|
|
766
|
+
}
|
|
767
|
+
entries.push({ name: dirent.name, mtimeMs, state });
|
|
768
|
+
}
|
|
769
|
+
return { entries, unfinished, finished, unreadable };
|
|
770
|
+
}
|
|
771
|
+
async function removeDanglingImages(docker, log, live) {
|
|
772
|
+
let freed = 0;
|
|
773
|
+
for (const image of await docker.listDanglingImages()) {
|
|
774
|
+
if (image.labels[KIND_LABEL] === void 0) continue;
|
|
775
|
+
if (live.has(image.id)) continue;
|
|
776
|
+
try {
|
|
777
|
+
await docker.removeImage(image.id);
|
|
778
|
+
freed += image.size;
|
|
779
|
+
log.line(`housekeeping: removed dangling image ${image.id.replace(/^sha256:/, "").slice(0, 12)} (${formatBytes(image.size)}), a rebuild replaced it`);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
log.error(`housekeeping: could not remove dangling image ${image.id}: ${describeCallFailure(err)}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return freed;
|
|
785
|
+
}
|
|
786
|
+
async function runHousekeeping(options) {
|
|
787
|
+
const { docker, log } = options;
|
|
788
|
+
const nowMs = options.now().getTime();
|
|
789
|
+
const root = runnerDir(options.configPath);
|
|
790
|
+
let freed = 0;
|
|
791
|
+
let listed;
|
|
792
|
+
try {
|
|
793
|
+
const containers = await docker.listBuildContainers();
|
|
794
|
+
listed = { containers, volumes: await docker.listBuildVolumes() };
|
|
795
|
+
} catch (err) {
|
|
796
|
+
log.error(`housekeeping: Docker could not be asked, so nothing in Docker is removed this pass: ${describeCallFailure(err)}`);
|
|
797
|
+
}
|
|
798
|
+
const jobs = readJobs(jobsDir(options.configPath));
|
|
799
|
+
for (const name of jobs.unreadable) {
|
|
800
|
+
log.error(`housekeeping: job file ${name}/job.json does not parse, so nothing in Docker is removed this pass`);
|
|
801
|
+
}
|
|
802
|
+
const canTell = jobs.unreadable.length === 0;
|
|
803
|
+
for (const entry of selectJobDirs(jobs.entries, nowMs, options.keepDays)) {
|
|
804
|
+
const path = join(jobsDir(options.configPath), entry.name);
|
|
805
|
+
const size = sizeOf(path);
|
|
806
|
+
try {
|
|
807
|
+
rmSync(path, { recursive: true, force: true });
|
|
808
|
+
freed += size;
|
|
809
|
+
log.line(`housekeeping: removed job directory ${entry.name} (${formatBytes(size)}), last changed ${formatDuration(nowMs - entry.mtimeMs)} ago`);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
log.error(`housekeeping: could not remove job directory ${entry.name}: ${describeCallFailure(err)}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const live = liveBuildIds(jobs.unfinished);
|
|
815
|
+
const finished = finishedBuildIds(jobs.finished);
|
|
816
|
+
if (canTell && listed !== void 0) {
|
|
817
|
+
for (const container of selectOrphans(listed.containers, live, finished)) {
|
|
818
|
+
try {
|
|
819
|
+
await docker.removeContainer(container.id);
|
|
820
|
+
log.line(`housekeeping: removed container ${container.id.slice(0, 12)}, no unfinished build ${container.build} names it`);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
log.error(`housekeeping: could not remove container ${container.id}: ${describeCallFailure(err)}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
for (const volume of selectOrphans(listed.volumes, live, finished)) {
|
|
826
|
+
try {
|
|
827
|
+
await docker.removeVolume(volume.name);
|
|
828
|
+
log.line(`housekeeping: removed volume ${volume.name}, no unfinished build ${volume.build} names it`);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
log.error(`housekeeping: could not remove volume ${volume.name}: ${describeCallFailure(err)}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (canTell && listed !== void 0) {
|
|
835
|
+
freed += await removeStaleRootDisks(root, jobs.unfinished, options);
|
|
836
|
+
try {
|
|
837
|
+
freed += await removeDanglingImages(docker, log, liveImageIds(jobs.unfinished));
|
|
838
|
+
} catch (err) {
|
|
839
|
+
log.error(`housekeeping: dangling images could not be listed: ${describeCallFailure(err)}`);
|
|
840
|
+
}
|
|
841
|
+
if (options.buildCacheMax !== "off") {
|
|
842
|
+
try {
|
|
843
|
+
const pruned = await docker.builderPrune(options.buildCacheMax);
|
|
844
|
+
freed += pruned.freedBytes;
|
|
845
|
+
log.line(`housekeeping: pruned the build cache down to ${formatBytes(options.buildCacheMax)}, freed ${formatBytes(pruned.freedBytes)}`);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
log.error(`housekeeping: the build cache could not be pruned: ${describeCallFailure(err)}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const spoolPath = spoolDir(root);
|
|
852
|
+
const spoolFiles = listDir(spoolPath).flatMap((name) => {
|
|
853
|
+
const mtimeMs = mtimeOf(join(spoolPath, name));
|
|
854
|
+
return mtimeMs === void 0 ? [] : [{ name, mtimeMs }];
|
|
855
|
+
});
|
|
856
|
+
for (const file of selectSpool(spoolFiles, nowMs, jobs.unfinished.length > 0)) {
|
|
857
|
+
const path = join(spoolPath, file.name);
|
|
858
|
+
const size = sizeOf(path);
|
|
859
|
+
try {
|
|
860
|
+
rmSync(path, { recursive: true, force: true });
|
|
861
|
+
freed += size;
|
|
862
|
+
log.line(`housekeeping: removed spool file ${file.name} (${formatBytes(size)}), last changed ${formatDuration(nowMs - file.mtimeMs)} ago and no build is running`);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
log.error(`housekeeping: could not remove spool file ${file.name}: ${describeCallFailure(err)}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
log.line(`housekeeping: pass finished, freed ${formatBytes(freed)}`);
|
|
868
|
+
return { freedBytes: freed };
|
|
869
|
+
}
|
|
870
|
+
async function removeStaleRootDisks(root, unfinished, options) {
|
|
871
|
+
const dir = rootfsDir(root);
|
|
872
|
+
const inUse = new Set(unfinished.flatMap((job) => job.microvm?.rootfs === void 0 ? [] : [job.microvm.rootfs]));
|
|
873
|
+
let freed = 0;
|
|
874
|
+
for (const name of listDir(dir)) {
|
|
875
|
+
const imageId = rootDiskImageId(name);
|
|
876
|
+
if (imageId === void 0) continue;
|
|
877
|
+
const path = join(dir, name);
|
|
878
|
+
if (inUse.has(path)) continue;
|
|
879
|
+
if (await options.docker.imageState(`sha256:${imageId}`) !== "missing") continue;
|
|
880
|
+
const size = sizeOf(path);
|
|
881
|
+
try {
|
|
882
|
+
rmSync(path, { force: true });
|
|
883
|
+
freed += size;
|
|
884
|
+
options.log.line(`housekeeping: removed root disk ${name} (${formatBytes(size)}), Docker no longer has its image`);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
options.log.error(`housekeeping: could not remove root disk ${name}: ${describeCallFailure(err)}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return freed;
|
|
890
|
+
}
|
|
891
|
+
|
|
528
892
|
// src/runner/loop.ts
|
|
529
893
|
var QUEUE_ORDER = ["amend", "implement"];
|
|
530
894
|
var TRACKER_REPOSITORY_HOSTS = {
|
|
531
895
|
github: "github.com",
|
|
532
896
|
gitlab: "gitlab.com"
|
|
533
897
|
};
|
|
898
|
+
var HOUSEKEEPING_INTERVAL_MS = 36e5;
|
|
534
899
|
function logOf(state) {
|
|
535
900
|
state.deps.log ??= createRunnerLog(state.deps.now ?? (() => /* @__PURE__ */ new Date()));
|
|
536
901
|
return state.deps.log;
|
|
@@ -645,7 +1010,9 @@ async function sendTickHeartbeat(state, workRequest) {
|
|
|
645
1010
|
slotsUsed: state.jobs.size,
|
|
646
1011
|
slotsTotal: state.config.concurrency,
|
|
647
1012
|
workRequest: next,
|
|
648
|
-
lastUpdate: readLastUpdate(state.config.configPath)
|
|
1013
|
+
lastUpdate: readLastUpdate(state.config.configPath),
|
|
1014
|
+
lowDisk: state.lowDisk,
|
|
1015
|
+
blocked: state.blocked
|
|
649
1016
|
},
|
|
650
1017
|
state.deps.fetchImpl
|
|
651
1018
|
);
|
|
@@ -769,9 +1136,84 @@ async function tick(state) {
|
|
|
769
1136
|
}
|
|
770
1137
|
await sendTickHeartbeat(state, state.lastWorkRequest);
|
|
771
1138
|
}
|
|
1139
|
+
function holdWhenBlocked(state) {
|
|
1140
|
+
const probes = {
|
|
1141
|
+
...realBlockedProbes(),
|
|
1142
|
+
...state.deps.container?.hostEnv === void 0 ? {} : { env: state.deps.container.hostEnv },
|
|
1143
|
+
...state.deps.blocked
|
|
1144
|
+
};
|
|
1145
|
+
const blocked = checkBlocked(state.config, probes);
|
|
1146
|
+
const log = logOf(state);
|
|
1147
|
+
if (blocked !== void 0) {
|
|
1148
|
+
if (state.blocked?.reason !== blocked.reason) log.line(`claiming nothing: ${blocked.reason}`);
|
|
1149
|
+
state.blocked = blocked;
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
if (state.blocked !== void 0) {
|
|
1153
|
+
state.blocked = void 0;
|
|
1154
|
+
log.line("the runner can run its work again \xB7 taking work");
|
|
1155
|
+
}
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
function holdOffForDisk(state) {
|
|
1159
|
+
const minimum = state.config.housekeeping?.minFreeDisk ?? 0;
|
|
1160
|
+
const root = runnerDir(state.config.configPath);
|
|
1161
|
+
const free = minimum === 0 ? Number.POSITIVE_INFINITY : (state.deps.freeBytes ?? freeBytes)(root) ?? Number.POSITIVE_INFINITY;
|
|
1162
|
+
const log = logOf(state);
|
|
1163
|
+
if (free < minimum) {
|
|
1164
|
+
if (state.lowDisk === void 0) {
|
|
1165
|
+
log.line(
|
|
1166
|
+
`holding off new work: ${formatBytes(free)} free on the runner's file system, ${formatBytes(minimum)} needed (runner.housekeeping.minFreeDisk) \xB7 claiming again once there is room`
|
|
1167
|
+
);
|
|
1168
|
+
recordHousekeeping(state, root, { holding: { free, needed: minimum, since: nowOf(state).toISOString() } });
|
|
1169
|
+
}
|
|
1170
|
+
state.lowDisk = { free, needed: minimum };
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
if (state.lowDisk !== void 0) {
|
|
1174
|
+
state.lowDisk = void 0;
|
|
1175
|
+
log.line("free disk is back above the minimum \xB7 taking work again");
|
|
1176
|
+
recordHousekeeping(state, root, { holding: void 0 });
|
|
1177
|
+
} else if (!state.holdRecordChecked) {
|
|
1178
|
+
if (readHousekeepingRecord(root).holding !== void 0) recordHousekeeping(state, root, { holding: void 0 });
|
|
1179
|
+
}
|
|
1180
|
+
state.holdRecordChecked = true;
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
function recordHousekeeping(state, root, patch) {
|
|
1184
|
+
try {
|
|
1185
|
+
writeHousekeepingRecord(root, patch);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
logOf(state).error(`could not write housekeeping.json: ${describeCallFailure(err)}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function housekeepingPass(state) {
|
|
1191
|
+
const config = state.config.housekeeping;
|
|
1192
|
+
if (config === void 0 || state.housekeepingRunning) return;
|
|
1193
|
+
state.housekeepingRunning = true;
|
|
1194
|
+
try {
|
|
1195
|
+
state.deps.housekeeping ??= {};
|
|
1196
|
+
state.deps.housekeeping.docker ??= createHousekeepingDocker({ runnerId: runnerLabelValue(state.config.configPath) });
|
|
1197
|
+
const result = await runHousekeeping({
|
|
1198
|
+
configPath: state.config.configPath,
|
|
1199
|
+
keepDays: config.jobsKeepDays,
|
|
1200
|
+
buildCacheMax: config.buildCacheMax,
|
|
1201
|
+
docker: state.deps.housekeeping.docker,
|
|
1202
|
+
now: state.deps.now ?? (() => /* @__PURE__ */ new Date()),
|
|
1203
|
+
log: logOf(state)
|
|
1204
|
+
});
|
|
1205
|
+
recordHousekeeping(state, runnerDir(state.config.configPath), { lastRunAt: nowOf(state).toISOString(), freedBytes: result.freedBytes });
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
logOf(state).error(`housekeeping failed: ${describeCallFailure(err)}`);
|
|
1208
|
+
} finally {
|
|
1209
|
+
state.housekeepingRunning = false;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
772
1212
|
async function runTick(state) {
|
|
773
1213
|
reloadConfig(state);
|
|
774
1214
|
if (await updateWhenIdle(state)) return;
|
|
1215
|
+
if (holdWhenBlocked(state)) return;
|
|
1216
|
+
if (holdOffForDisk(state)) return;
|
|
775
1217
|
const log = logOf(state);
|
|
776
1218
|
const repoFilter = new RepoFilter(state.config.repos);
|
|
777
1219
|
let filtered = 0;
|
|
@@ -825,7 +1267,7 @@ var CLAIM_REFUSAL_CODES = new Set(
|
|
|
825
1267
|
"pull_request_from_fork pull_request_on_default_branch pull_request_ambiguous branch_claimed_elsewhere".split(" ")
|
|
826
1268
|
);
|
|
827
1269
|
async function tryClaimAndStart(state, item, queue) {
|
|
828
|
-
const { postJson: postJson2 } = await import('./work-cmd-
|
|
1270
|
+
const { postJson: postJson2 } = await import('./work-cmd-OVNF44FY.js');
|
|
829
1271
|
const log = logOf(state);
|
|
830
1272
|
const hostKey = await loadHostKey(state.config.configPath);
|
|
831
1273
|
const claim = await retrySend(
|
|
@@ -1304,6 +1746,17 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity)
|
|
|
1304
1746
|
writeJobFile(jobFilePath, job);
|
|
1305
1747
|
const commandStartedMs = nowOf(state).getTime();
|
|
1306
1748
|
log.line(`${tag} command started \xB7 pid ${running.pid}`);
|
|
1749
|
+
const sender = await startSender({
|
|
1750
|
+
config: state.config,
|
|
1751
|
+
job,
|
|
1752
|
+
jobDir: dirPath,
|
|
1753
|
+
deps: { spawnFn: state.deps.spawnFn, runtime: job.microvm !== void 0 || job.container !== void 0 ? runtimeOf(state) : void 0, log }
|
|
1754
|
+
});
|
|
1755
|
+
if (sender !== void 0) {
|
|
1756
|
+
job = { ...active.job, sender };
|
|
1757
|
+
active.job = job;
|
|
1758
|
+
writeJobFile(jobFilePath, job);
|
|
1759
|
+
}
|
|
1307
1760
|
running.onExit.then(({ code, signal, error }) => {
|
|
1308
1761
|
if (active.ending) return;
|
|
1309
1762
|
if (error !== void 0) {
|
|
@@ -1525,7 +1978,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
|
|
|
1525
1978
|
const outcome = await onStartUnderMarker({
|
|
1526
1979
|
configPath,
|
|
1527
1980
|
own: ownVersionOf(deps),
|
|
1528
|
-
check: update.check ? () => update.check(configPath) : async () => (await import('./check-
|
|
1981
|
+
check: update.check ? () => update.check(configPath) : async () => (await import('./check-CCVPBN7A.js')).runRunnerCheck(configPath, { fetchImpl: deps.fetchImpl }),
|
|
1529
1982
|
now: () => (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime(),
|
|
1530
1983
|
log
|
|
1531
1984
|
});
|
|
@@ -1544,6 +1997,11 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
|
|
|
1544
1997
|
mkdirSync(jobsDir(configPath), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1545
1998
|
const self = { pid: process.pid, start: startTimeOf(process.pid) ?? "" };
|
|
1546
1999
|
claimPidFile(pidFilePath(configPath), self);
|
|
2000
|
+
const { delegated, refusal } = setUpRunnerCgroup();
|
|
2001
|
+
if (refusal !== void 0) log.error(`the kernel refused a cgroup step for the runner's service: ${refusal}`);
|
|
2002
|
+
if (isBuiltinVm(config.hooks.provision) && !delegated) {
|
|
2003
|
+
log.line("microVM builds run with no memory limit of their own: the runner's service is not delegated its cgroup (Delegate=yes in the unit, cgroup v2)");
|
|
2004
|
+
}
|
|
1547
2005
|
let configMtimeMs;
|
|
1548
2006
|
try {
|
|
1549
2007
|
configMtimeMs = statSync(configPath).mtimeMs;
|
|
@@ -1606,6 +2064,12 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
|
|
|
1606
2064
|
let interrupts = 0;
|
|
1607
2065
|
state.pollTimerSeconds = state.config.pollSeconds;
|
|
1608
2066
|
const pollTimer = setInterval(() => void tick(state), state.pollTimerSeconds * 1e3);
|
|
2067
|
+
const hooksKind = builtinKind(config.hooks.provision);
|
|
2068
|
+
const housekeepingTimer = hooksKind === "container" || hooksKind === "vm" ? (() => {
|
|
2069
|
+
const everyMs = deps.housekeeping?.intervalMs ?? HOUSEKEEPING_INTERVAL_MS;
|
|
2070
|
+
void housekeepingPass(state);
|
|
2071
|
+
return setInterval(() => void housekeepingPass(state), everyMs);
|
|
2072
|
+
})() : void 0;
|
|
1609
2073
|
const watchTimer = setInterval(() => {
|
|
1610
2074
|
const now = nowMs();
|
|
1611
2075
|
for (const active of state.jobs.values()) {
|
|
@@ -1639,6 +2103,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
|
|
|
1639
2103
|
if (intakeClosed) return;
|
|
1640
2104
|
intakeClosed = true;
|
|
1641
2105
|
clearInterval(pollTimer);
|
|
2106
|
+
clearInterval(housekeepingTimer);
|
|
1642
2107
|
};
|
|
1643
2108
|
const close = () => {
|
|
1644
2109
|
stopIntake();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { detectMachine, supportsContainerBuilds, reachDockerStep, describeMachineDocker, supportsMicroVmBuilds } from './chunk-
|
|
4
|
-
import { MIN_ENGINE_MAJOR } from './chunk-
|
|
3
|
+
import { detectMachine, supportsContainerBuilds, reachDockerStep, describeMachineDocker, supportsMicroVmBuilds } from './chunk-RBOMWNOG.js';
|
|
4
|
+
import { MIN_ENGINE_MAJOR } from './chunk-RZSF5B46.js';
|
|
5
5
|
import { RUNNER_DIR_MODE, runnerDir, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
6
6
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
7
7
|
import { dirname } from 'path';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { sanitizeTerminal } from './chunk-
|
|
3
|
+
import { sanitizeTerminal } from './chunk-3DEVYTNQ.js';
|
|
4
4
|
import { DEFAULT_DASHBOARD } from './chunk-USO2DKBD.js';
|
|
5
5
|
import { readCloudConfig } from './chunk-EABW6AJQ.js';
|
|
6
6
|
|