@vm0/runner 3.12.0 → 3.12.1
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/index.js +152 -63
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -416,6 +416,37 @@ async function withFileLock(path9, fn, options) {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
// src/lib/utils/process.ts
|
|
420
|
+
import { execSync } from "child_process";
|
|
421
|
+
function isProcessRunning(pid) {
|
|
422
|
+
try {
|
|
423
|
+
process.kill(pid, 0);
|
|
424
|
+
return true;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (err instanceof Error && "code" in err && err.code === "EPERM") {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function killProcessTree(pid) {
|
|
433
|
+
try {
|
|
434
|
+
const childPidsStr = execSync(`pgrep -P ${pid} 2>/dev/null || true`, {
|
|
435
|
+
encoding: "utf-8"
|
|
436
|
+
}).trim();
|
|
437
|
+
if (childPidsStr) {
|
|
438
|
+
const childPids = childPidsStr.split("\n").map((p) => parseInt(p, 10));
|
|
439
|
+
for (const childPid of childPids) {
|
|
440
|
+
if (!isNaN(childPid)) {
|
|
441
|
+
killProcessTree(childPid);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
process.kill(pid, "SIGKILL");
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
419
450
|
// src/lib/utils/exec.ts
|
|
420
451
|
import { exec } from "child_process";
|
|
421
452
|
import { promisify } from "util";
|
|
@@ -549,14 +580,6 @@ function makeNsName(runnerIdx, nsIdx) {
|
|
|
549
580
|
function makeVethName(runnerIdx, nsIdx) {
|
|
550
581
|
return `${VETH_PREFIX}${runnerIdx}-${nsIdx}`;
|
|
551
582
|
}
|
|
552
|
-
function isPidAlive(pid) {
|
|
553
|
-
try {
|
|
554
|
-
process.kill(pid, 0);
|
|
555
|
-
return true;
|
|
556
|
-
} catch {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
583
|
async function deleteIptablesRulesByComment(comment) {
|
|
561
584
|
const deleteFromTable = async (table) => {
|
|
562
585
|
try {
|
|
@@ -625,7 +648,7 @@ var NetnsPool = class _NetnsPool {
|
|
|
625
648
|
const data = read();
|
|
626
649
|
const orphaned = [];
|
|
627
650
|
for (const [runnerIdx, runner] of Object.entries(data.runners)) {
|
|
628
|
-
if (!
|
|
651
|
+
if (!isProcessRunning(runner.pid)) {
|
|
629
652
|
orphaned.push({
|
|
630
653
|
runnerIdx,
|
|
631
654
|
namespaces: Object.entries(runner.namespaces).map(
|
|
@@ -662,7 +685,7 @@ var NetnsPool = class _NetnsPool {
|
|
|
662
685
|
const data = read();
|
|
663
686
|
for (const { runnerIdx } of orphanedData) {
|
|
664
687
|
const runner = data.runners[runnerIdx];
|
|
665
|
-
if (runner && !
|
|
688
|
+
if (runner && !isProcessRunning(runner.pid)) {
|
|
666
689
|
delete data.runners[runnerIdx];
|
|
667
690
|
}
|
|
668
691
|
}
|
|
@@ -1671,8 +1694,8 @@ var FirecrackerVM = class {
|
|
|
1671
1694
|
* since we want to clean up as much as possible even if some parts fail.
|
|
1672
1695
|
*/
|
|
1673
1696
|
async cleanup() {
|
|
1674
|
-
if (this.process && !this.process.killed) {
|
|
1675
|
-
this.process.
|
|
1697
|
+
if (this.process && !this.process.killed && this.process.pid) {
|
|
1698
|
+
killProcessTree(this.process.pid);
|
|
1676
1699
|
this.process = null;
|
|
1677
1700
|
}
|
|
1678
1701
|
if (this.netns) {
|
|
@@ -7884,6 +7907,7 @@ var modelProviderTypeSchema = z19.enum([
|
|
|
7884
7907
|
"minimax-api-key",
|
|
7885
7908
|
"deepseek-api-key",
|
|
7886
7909
|
"zai-api-key",
|
|
7910
|
+
"azure-foundry",
|
|
7887
7911
|
"aws-bedrock"
|
|
7888
7912
|
]);
|
|
7889
7913
|
var modelProviderFrameworkSchema = z19.enum(["claude-code", "codex"]);
|
|
@@ -10472,12 +10496,12 @@ function createStatusUpdater(statusFilePath, state) {
|
|
|
10472
10496
|
}
|
|
10473
10497
|
|
|
10474
10498
|
// src/lib/firecracker/network.ts
|
|
10475
|
-
import { execSync, exec as exec3 } from "child_process";
|
|
10499
|
+
import { execSync as execSync2, exec as exec3 } from "child_process";
|
|
10476
10500
|
import { promisify as promisify3 } from "util";
|
|
10477
10501
|
var execAsync3 = promisify3(exec3);
|
|
10478
10502
|
function commandExists(cmd) {
|
|
10479
10503
|
try {
|
|
10480
|
-
|
|
10504
|
+
execSync2(`which ${cmd}`, { stdio: "ignore" });
|
|
10481
10505
|
return true;
|
|
10482
10506
|
} catch {
|
|
10483
10507
|
return false;
|
|
@@ -10492,7 +10516,7 @@ function checkNetworkPrerequisites() {
|
|
|
10492
10516
|
}
|
|
10493
10517
|
}
|
|
10494
10518
|
try {
|
|
10495
|
-
|
|
10519
|
+
execSync2("sudo -n true 2>/dev/null", { stdio: "ignore" });
|
|
10496
10520
|
} catch {
|
|
10497
10521
|
errors.push(
|
|
10498
10522
|
"Root/sudo access required for network configuration. Please run with sudo or configure sudoers."
|
|
@@ -10518,17 +10542,6 @@ import path7 from "path";
|
|
|
10518
10542
|
var logger11 = createLogger("RunnerLock");
|
|
10519
10543
|
var DEFAULT_PID_FILE = runtimePaths.runnerPid;
|
|
10520
10544
|
var currentPidFile = null;
|
|
10521
|
-
function isProcessRunning(pid) {
|
|
10522
|
-
try {
|
|
10523
|
-
process.kill(pid, 0);
|
|
10524
|
-
return true;
|
|
10525
|
-
} catch (err) {
|
|
10526
|
-
if (err instanceof Error && "code" in err && err.code === "EPERM") {
|
|
10527
|
-
return true;
|
|
10528
|
-
}
|
|
10529
|
-
return false;
|
|
10530
|
-
}
|
|
10531
|
-
}
|
|
10532
10545
|
function acquireRunnerLock(options = {}) {
|
|
10533
10546
|
const pidFile = options.pidFile ?? DEFAULT_PID_FILE;
|
|
10534
10547
|
const runDir = path7.dirname(pidFile);
|
|
@@ -10909,6 +10922,7 @@ var startCommand = new Command("start").description("Start the runner").option("
|
|
|
10909
10922
|
// src/commands/doctor.ts
|
|
10910
10923
|
import { Command as Command2 } from "commander";
|
|
10911
10924
|
import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
10925
|
+
import { execSync as execSync3 } from "child_process";
|
|
10912
10926
|
|
|
10913
10927
|
// src/lib/firecracker/process.ts
|
|
10914
10928
|
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
@@ -10916,12 +10930,21 @@ import path8 from "path";
|
|
|
10916
10930
|
function parseFirecrackerCmdline(cmdline) {
|
|
10917
10931
|
const args = cmdline.split("\0");
|
|
10918
10932
|
if (!args[0]?.includes("firecracker")) return null;
|
|
10933
|
+
let filePath;
|
|
10919
10934
|
const sockIdx = args.indexOf("--api-sock");
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10935
|
+
if (sockIdx !== -1) {
|
|
10936
|
+
filePath = args[sockIdx + 1];
|
|
10937
|
+
}
|
|
10938
|
+
if (!filePath) {
|
|
10939
|
+
const configIdx = args.indexOf("--config-file");
|
|
10940
|
+
if (configIdx !== -1) {
|
|
10941
|
+
filePath = args[configIdx + 1];
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10944
|
+
if (!filePath) return null;
|
|
10945
|
+
const match = filePath.match(/vm0-([a-f0-9]+)\//);
|
|
10923
10946
|
if (!match?.[1]) return null;
|
|
10924
|
-
return
|
|
10947
|
+
return createVmId(match[1]);
|
|
10925
10948
|
}
|
|
10926
10949
|
function parseMitmproxyCmdline(cmdline) {
|
|
10927
10950
|
if (!cmdline.includes("mitmproxy") && !cmdline.includes("mitmdump")) {
|
|
@@ -10949,9 +10972,9 @@ function findFirecrackerProcesses() {
|
|
|
10949
10972
|
if (!existsSync4(cmdlinePath)) continue;
|
|
10950
10973
|
try {
|
|
10951
10974
|
const cmdline = readFileSync2(cmdlinePath, "utf-8");
|
|
10952
|
-
const
|
|
10953
|
-
if (
|
|
10954
|
-
processes.push({ pid,
|
|
10975
|
+
const vmId = parseFirecrackerCmdline(cmdline);
|
|
10976
|
+
if (vmId) {
|
|
10977
|
+
processes.push({ pid, vmId });
|
|
10955
10978
|
}
|
|
10956
10979
|
} catch {
|
|
10957
10980
|
continue;
|
|
@@ -10964,33 +10987,25 @@ function findProcessByVmId(vmId) {
|
|
|
10964
10987
|
const vmIdStr = vmIdValue(vmId);
|
|
10965
10988
|
return processes.find((p) => vmIdValue(p.vmId) === vmIdStr) || null;
|
|
10966
10989
|
}
|
|
10967
|
-
function isProcessRunning2(pid) {
|
|
10968
|
-
try {
|
|
10969
|
-
process.kill(pid, 0);
|
|
10970
|
-
return true;
|
|
10971
|
-
} catch {
|
|
10972
|
-
return false;
|
|
10973
|
-
}
|
|
10974
|
-
}
|
|
10975
10990
|
async function killProcess(pid, timeoutMs = 5e3) {
|
|
10976
|
-
if (!
|
|
10991
|
+
if (!isProcessRunning(pid)) return true;
|
|
10977
10992
|
try {
|
|
10978
10993
|
process.kill(pid, "SIGTERM");
|
|
10979
10994
|
} catch {
|
|
10980
|
-
return !
|
|
10995
|
+
return !isProcessRunning(pid);
|
|
10981
10996
|
}
|
|
10982
10997
|
const startTime = Date.now();
|
|
10983
10998
|
while (Date.now() - startTime < timeoutMs) {
|
|
10984
|
-
if (!
|
|
10999
|
+
if (!isProcessRunning(pid)) return true;
|
|
10985
11000
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
10986
11001
|
}
|
|
10987
|
-
if (
|
|
11002
|
+
if (isProcessRunning(pid)) {
|
|
10988
11003
|
try {
|
|
10989
11004
|
process.kill(pid, "SIGKILL");
|
|
10990
11005
|
} catch {
|
|
10991
11006
|
}
|
|
10992
11007
|
}
|
|
10993
|
-
return !
|
|
11008
|
+
return !isProcessRunning(pid);
|
|
10994
11009
|
}
|
|
10995
11010
|
function findMitmproxyProcess() {
|
|
10996
11011
|
const procDir = "/proc";
|
|
@@ -11018,15 +11033,26 @@ function findMitmproxyProcess() {
|
|
|
11018
11033
|
return null;
|
|
11019
11034
|
}
|
|
11020
11035
|
|
|
11036
|
+
// src/lib/runner/types.ts
|
|
11037
|
+
import { z as z30 } from "zod";
|
|
11038
|
+
var RunnerModeSchema = z30.enum(["running", "draining", "stopping", "stopped"]);
|
|
11039
|
+
var RunnerStatusSchema = z30.object({
|
|
11040
|
+
mode: RunnerModeSchema,
|
|
11041
|
+
active_runs: z30.number(),
|
|
11042
|
+
active_run_ids: z30.array(z30.string()),
|
|
11043
|
+
started_at: z30.string(),
|
|
11044
|
+
updated_at: z30.string()
|
|
11045
|
+
});
|
|
11046
|
+
|
|
11021
11047
|
// src/commands/doctor.ts
|
|
11022
|
-
function displayRunnerStatus(statusFilePath) {
|
|
11048
|
+
function displayRunnerStatus(statusFilePath, warnings) {
|
|
11023
11049
|
if (!existsSync5(statusFilePath)) {
|
|
11024
11050
|
console.log("Mode: unknown (no status.json)");
|
|
11025
11051
|
return null;
|
|
11026
11052
|
}
|
|
11027
11053
|
try {
|
|
11028
|
-
const status =
|
|
11029
|
-
readFileSync3(statusFilePath, "utf-8")
|
|
11054
|
+
const status = RunnerStatusSchema.parse(
|
|
11055
|
+
JSON.parse(readFileSync3(statusFilePath, "utf-8"))
|
|
11030
11056
|
);
|
|
11031
11057
|
console.log(`Mode: ${status.mode}`);
|
|
11032
11058
|
if (status.started_at) {
|
|
@@ -11037,10 +11063,11 @@ function displayRunnerStatus(statusFilePath) {
|
|
|
11037
11063
|
return status;
|
|
11038
11064
|
} catch {
|
|
11039
11065
|
console.log("Mode: unknown (status.json unreadable)");
|
|
11066
|
+
warnings.push({ message: "status.json exists but cannot be parsed" });
|
|
11040
11067
|
return null;
|
|
11041
11068
|
}
|
|
11042
11069
|
}
|
|
11043
|
-
async function checkApiConnectivity(config) {
|
|
11070
|
+
async function checkApiConnectivity(config, warnings) {
|
|
11044
11071
|
console.log("API Connectivity:");
|
|
11045
11072
|
try {
|
|
11046
11073
|
await pollForJob(config.server, config.group);
|
|
@@ -11051,6 +11078,9 @@ async function checkApiConnectivity(config) {
|
|
|
11051
11078
|
console.log(
|
|
11052
11079
|
` Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11053
11080
|
);
|
|
11081
|
+
warnings.push({
|
|
11082
|
+
message: `Cannot connect to API: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11083
|
+
});
|
|
11054
11084
|
}
|
|
11055
11085
|
}
|
|
11056
11086
|
async function checkNetwork(config, warnings) {
|
|
@@ -11086,8 +11116,7 @@ function buildJobInfo(status, processes) {
|
|
|
11086
11116
|
jobs.push({
|
|
11087
11117
|
runId,
|
|
11088
11118
|
vmId,
|
|
11089
|
-
|
|
11090
|
-
pid: proc?.pid
|
|
11119
|
+
firecrackerPid: proc?.pid
|
|
11091
11120
|
});
|
|
11092
11121
|
}
|
|
11093
11122
|
}
|
|
@@ -11101,13 +11130,61 @@ function displayRuns(jobs, maxConcurrent) {
|
|
|
11101
11130
|
}
|
|
11102
11131
|
console.log(" Run ID VM ID Status");
|
|
11103
11132
|
for (const job of jobs) {
|
|
11104
|
-
const statusText = job.
|
|
11133
|
+
const statusText = job.firecrackerPid ? `\u2713 Running (PID ${job.firecrackerPid})` : "\u26A0\uFE0F No process";
|
|
11105
11134
|
console.log(` ${job.runId} ${job.vmId} ${statusText}`);
|
|
11106
11135
|
}
|
|
11107
11136
|
}
|
|
11108
|
-
function
|
|
11137
|
+
async function findOrphanNetworkNamespaces(warnings) {
|
|
11138
|
+
let allNamespaces = [];
|
|
11139
|
+
try {
|
|
11140
|
+
const output = execSync3("ip netns list 2>/dev/null || true", {
|
|
11141
|
+
encoding: "utf-8"
|
|
11142
|
+
});
|
|
11143
|
+
allNamespaces = output.split("\n").map((line) => line.split(" ")[0] ?? "").filter((ns) => ns.startsWith(NS_PREFIX));
|
|
11144
|
+
} catch (err) {
|
|
11145
|
+
warnings.push({
|
|
11146
|
+
message: `Failed to list network namespaces: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
11147
|
+
});
|
|
11148
|
+
return [];
|
|
11149
|
+
}
|
|
11150
|
+
if (allNamespaces.length === 0) {
|
|
11151
|
+
return [];
|
|
11152
|
+
}
|
|
11153
|
+
const registryPath = runtimePaths.netnsRegistry;
|
|
11154
|
+
if (!existsSync5(registryPath)) {
|
|
11155
|
+
return allNamespaces;
|
|
11156
|
+
}
|
|
11157
|
+
try {
|
|
11158
|
+
return await withFileLock(registryPath, async () => {
|
|
11159
|
+
const registry = RegistrySchema.parse(
|
|
11160
|
+
JSON.parse(readFileSync3(registryPath, "utf-8"))
|
|
11161
|
+
);
|
|
11162
|
+
const aliveNamespaces = /* @__PURE__ */ new Set();
|
|
11163
|
+
for (const [runnerIdx, runner] of Object.entries(registry.runners)) {
|
|
11164
|
+
if (isProcessRunning(runner.pid)) {
|
|
11165
|
+
for (const nsIdx of Object.keys(runner.namespaces)) {
|
|
11166
|
+
aliveNamespaces.add(`${NS_PREFIX}${runnerIdx}-${nsIdx}`);
|
|
11167
|
+
}
|
|
11168
|
+
}
|
|
11169
|
+
}
|
|
11170
|
+
const orphans = [];
|
|
11171
|
+
for (const ns of allNamespaces) {
|
|
11172
|
+
if (!aliveNamespaces.has(ns)) {
|
|
11173
|
+
orphans.push(ns);
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
return orphans;
|
|
11177
|
+
});
|
|
11178
|
+
} catch (err) {
|
|
11179
|
+
warnings.push({
|
|
11180
|
+
message: `Failed to read netns registry: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
11181
|
+
});
|
|
11182
|
+
return [];
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
async function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings) {
|
|
11109
11186
|
for (const job of jobs) {
|
|
11110
|
-
if (!job.
|
|
11187
|
+
if (!job.firecrackerPid) {
|
|
11111
11188
|
warnings.push({
|
|
11112
11189
|
message: `Run ${job.vmId} in status.json but no Firecracker process running`
|
|
11113
11190
|
});
|
|
@@ -11121,6 +11198,12 @@ function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warning
|
|
|
11121
11198
|
});
|
|
11122
11199
|
}
|
|
11123
11200
|
}
|
|
11201
|
+
const orphanNetns = await findOrphanNetworkNamespaces(warnings);
|
|
11202
|
+
for (const ns of orphanNetns) {
|
|
11203
|
+
warnings.push({
|
|
11204
|
+
message: `Orphan network namespace: ${ns} (runner process not running)`
|
|
11205
|
+
});
|
|
11206
|
+
}
|
|
11124
11207
|
for (const ws of workspaces) {
|
|
11125
11208
|
const vmId = runnerPaths.extractVmId(ws);
|
|
11126
11209
|
if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
|
|
@@ -11157,9 +11240,9 @@ var doctorCommand = new Command2("doctor").description("Diagnose runner health,
|
|
|
11157
11240
|
const workspacesDir = runnerPaths.workspacesDir(config.base_dir);
|
|
11158
11241
|
const warnings = [];
|
|
11159
11242
|
console.log(`Runner: ${config.name}`);
|
|
11160
|
-
const status = displayRunnerStatus(statusFilePath);
|
|
11243
|
+
const status = displayRunnerStatus(statusFilePath, warnings);
|
|
11161
11244
|
console.log("");
|
|
11162
|
-
await checkApiConnectivity(config);
|
|
11245
|
+
await checkApiConnectivity(config, warnings);
|
|
11163
11246
|
console.log("");
|
|
11164
11247
|
await checkNetwork(config, warnings);
|
|
11165
11248
|
console.log("");
|
|
@@ -11168,7 +11251,13 @@ var doctorCommand = new Command2("doctor").description("Diagnose runner health,
|
|
|
11168
11251
|
const { jobs, statusVmIds } = buildJobInfo(status, processes);
|
|
11169
11252
|
displayRuns(jobs, config.sandbox.max_concurrent);
|
|
11170
11253
|
console.log("");
|
|
11171
|
-
detectOrphanResources(
|
|
11254
|
+
await detectOrphanResources(
|
|
11255
|
+
jobs,
|
|
11256
|
+
processes,
|
|
11257
|
+
workspaces,
|
|
11258
|
+
statusVmIds,
|
|
11259
|
+
warnings
|
|
11260
|
+
);
|
|
11172
11261
|
displayWarnings(warnings);
|
|
11173
11262
|
process.exit(warnings.length > 0 ? 1 : 0);
|
|
11174
11263
|
} catch (error) {
|
|
@@ -11251,8 +11340,8 @@ var killCommand = new Command3("kill").description("Force terminate a run and cl
|
|
|
11251
11340
|
}
|
|
11252
11341
|
if (runId && existsSync6(statusFilePath)) {
|
|
11253
11342
|
try {
|
|
11254
|
-
const status =
|
|
11255
|
-
readFileSync4(statusFilePath, "utf-8")
|
|
11343
|
+
const status = RunnerStatusSchema.parse(
|
|
11344
|
+
JSON.parse(readFileSync4(statusFilePath, "utf-8"))
|
|
11256
11345
|
);
|
|
11257
11346
|
const oldCount = status.active_runs;
|
|
11258
11347
|
status.active_run_ids = status.active_run_ids.filter(
|
|
@@ -11309,8 +11398,8 @@ function resolveRunId(input, statusFilePath) {
|
|
|
11309
11398
|
}
|
|
11310
11399
|
if (existsSync6(statusFilePath)) {
|
|
11311
11400
|
try {
|
|
11312
|
-
const status =
|
|
11313
|
-
readFileSync4(statusFilePath, "utf-8")
|
|
11401
|
+
const status = RunnerStatusSchema.parse(
|
|
11402
|
+
JSON.parse(readFileSync4(statusFilePath, "utf-8"))
|
|
11314
11403
|
);
|
|
11315
11404
|
const match = status.active_run_ids.find(
|
|
11316
11405
|
(id) => id.startsWith(input)
|
|
@@ -11658,7 +11747,7 @@ var snapshotCommand = new Command5("snapshot").description("Generate a Firecrack
|
|
|
11658
11747
|
);
|
|
11659
11748
|
|
|
11660
11749
|
// src/index.ts
|
|
11661
|
-
var version = true ? "3.12.
|
|
11750
|
+
var version = true ? "3.12.1" : "0.1.0";
|
|
11662
11751
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
11663
11752
|
program.addCommand(startCommand);
|
|
11664
11753
|
program.addCommand(doctorCommand);
|