@vm0/runner 3.11.3 → 3.12.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/index.js +506 -202
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var runnerPaths = {
|
|
|
44
44
|
/** Runner status file */
|
|
45
45
|
statusFile: (baseDir) => path.join(baseDir, "status.json"),
|
|
46
46
|
/** Snapshot generation work directory */
|
|
47
|
-
snapshotWorkDir: (baseDir) => path.join(baseDir, "workspaces", "
|
|
47
|
+
snapshotWorkDir: (baseDir) => path.join(baseDir, "workspaces", "snapshot"),
|
|
48
48
|
/** Check if a directory name is a VM workspace */
|
|
49
49
|
isVmWorkspace: (dirname) => dirname.startsWith(VM_WORKSPACE_PREFIX),
|
|
50
50
|
/** Extract vmId from workspace directory name */
|
|
@@ -62,6 +62,14 @@ var vmPaths = {
|
|
|
62
62
|
/** Overlay filesystem for VM writes */
|
|
63
63
|
overlay: (workDir) => path.join(workDir, "overlay.ext4")
|
|
64
64
|
};
|
|
65
|
+
var snapshotOutputPaths = {
|
|
66
|
+
/** VM state snapshot */
|
|
67
|
+
snapshot: (outputDir) => path.join(outputDir, "snapshot.bin"),
|
|
68
|
+
/** VM memory snapshot */
|
|
69
|
+
memory: (outputDir) => path.join(outputDir, "memory.bin"),
|
|
70
|
+
/** Golden overlay with guest state */
|
|
71
|
+
overlay: (outputDir) => path.join(outputDir, "overlay.ext4")
|
|
72
|
+
};
|
|
65
73
|
var tempPaths = {
|
|
66
74
|
/** Default proxy CA directory */
|
|
67
75
|
proxyDir: `${VM0_TMP_PREFIX}-proxy`,
|
|
@@ -102,7 +110,12 @@ var runnerConfigSchema = z.object({
|
|
|
102
110
|
firecracker: z.object({
|
|
103
111
|
binary: z.string().min(1, "Firecracker binary path is required"),
|
|
104
112
|
kernel: z.string().min(1, "Kernel path is required"),
|
|
105
|
-
rootfs: z.string().min(1, "Rootfs path is required")
|
|
113
|
+
rootfs: z.string().min(1, "Rootfs path is required"),
|
|
114
|
+
snapshot: z.object({
|
|
115
|
+
snapshot: z.string().min(1, "Snapshot state file path is required"),
|
|
116
|
+
memory: z.string().min(1, "Snapshot memory file path is required"),
|
|
117
|
+
overlay: z.string().min(1, "Snapshot overlay file path is required")
|
|
118
|
+
}).optional()
|
|
106
119
|
}),
|
|
107
120
|
proxy: z.object({
|
|
108
121
|
// TODO: Allow 0 to auto-find available port
|
|
@@ -131,7 +144,12 @@ var debugConfigSchema = z.object({
|
|
|
131
144
|
firecracker: z.object({
|
|
132
145
|
binary: z.string().min(1, "Firecracker binary path is required"),
|
|
133
146
|
kernel: z.string().min(1, "Kernel path is required"),
|
|
134
|
-
rootfs: z.string().min(1, "Rootfs path is required")
|
|
147
|
+
rootfs: z.string().min(1, "Rootfs path is required"),
|
|
148
|
+
snapshot: z.object({
|
|
149
|
+
snapshot: z.string().min(1, "Snapshot state file path is required"),
|
|
150
|
+
memory: z.string().min(1, "Snapshot memory file path is required"),
|
|
151
|
+
overlay: z.string().min(1, "Snapshot overlay file path is required")
|
|
152
|
+
}).optional()
|
|
135
153
|
}),
|
|
136
154
|
proxy: z.object({
|
|
137
155
|
port: z.number().int().min(1024).max(65535).default(PROXY_DEFAULTS.port),
|
|
@@ -172,6 +190,13 @@ function validateFirecrackerPaths(config) {
|
|
|
172
190
|
{ path: config.kernel, name: "Kernel" },
|
|
173
191
|
{ path: config.rootfs, name: "Rootfs" }
|
|
174
192
|
];
|
|
193
|
+
if (config.snapshot) {
|
|
194
|
+
checks.push(
|
|
195
|
+
{ path: config.snapshot.snapshot, name: "Snapshot state file" },
|
|
196
|
+
{ path: config.snapshot.memory, name: "Snapshot memory file" },
|
|
197
|
+
{ path: config.snapshot.overlay, name: "Snapshot overlay file" }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
175
200
|
for (const check of checks) {
|
|
176
201
|
if (!fs.existsSync(check.path)) {
|
|
177
202
|
throw new Error(`${check.name} not found: ${check.path}`);
|
|
@@ -337,10 +362,13 @@ async function subscribeToJobs(server, group, onJob, onConnectionChange) {
|
|
|
337
362
|
|
|
338
363
|
// src/lib/executor.ts
|
|
339
364
|
import fs9 from "fs";
|
|
365
|
+
import path6 from "path";
|
|
340
366
|
|
|
341
367
|
// src/lib/firecracker/vm.ts
|
|
342
368
|
import { spawn } from "child_process";
|
|
343
369
|
import fs4 from "fs";
|
|
370
|
+
import os from "os";
|
|
371
|
+
import path4 from "path";
|
|
344
372
|
import readline from "readline";
|
|
345
373
|
|
|
346
374
|
// src/lib/firecracker/netns-pool.ts
|
|
@@ -379,8 +407,8 @@ var DEFAULT_OPTIONS = {
|
|
|
379
407
|
maxTimeout: 1e3
|
|
380
408
|
}
|
|
381
409
|
};
|
|
382
|
-
async function withFileLock(
|
|
383
|
-
const release = await lockfile.lock(
|
|
410
|
+
async function withFileLock(path9, fn, options) {
|
|
411
|
+
const release = await lockfile.lock(path9, { ...DEFAULT_OPTIONS, ...options });
|
|
384
412
|
try {
|
|
385
413
|
return await fn();
|
|
386
414
|
} finally {
|
|
@@ -436,6 +464,10 @@ async function createNetnsWithTap(nsName, tap) {
|
|
|
436
464
|
await execCommand(`ip netns exec ${nsName} ip link set ${tap.tapName} up`);
|
|
437
465
|
await execCommand(`ip netns exec ${nsName} ip link set lo up`);
|
|
438
466
|
}
|
|
467
|
+
async function deleteNetns(nsName) {
|
|
468
|
+
await execCommand(`ip netns del ${nsName}`).catch(() => {
|
|
469
|
+
});
|
|
470
|
+
}
|
|
439
471
|
|
|
440
472
|
// src/lib/firecracker/netns-pool.ts
|
|
441
473
|
var logger = createLogger("NetnsPool");
|
|
@@ -1077,10 +1109,10 @@ import * as http from "http";
|
|
|
1077
1109
|
import * as fs3 from "fs";
|
|
1078
1110
|
var logger3 = createLogger("FirecrackerClient");
|
|
1079
1111
|
var FirecrackerApiError = class extends Error {
|
|
1080
|
-
constructor(statusCode,
|
|
1081
|
-
super(`Firecracker API error ${statusCode} on ${
|
|
1112
|
+
constructor(statusCode, path9, faultMessage) {
|
|
1113
|
+
super(`Firecracker API error ${statusCode} on ${path9}: ${faultMessage}`);
|
|
1082
1114
|
this.statusCode = statusCode;
|
|
1083
|
-
this.path =
|
|
1115
|
+
this.path = path9;
|
|
1084
1116
|
this.faultMessage = faultMessage;
|
|
1085
1117
|
this.name = "FirecrackerApiError";
|
|
1086
1118
|
}
|
|
@@ -1193,27 +1225,27 @@ var FirecrackerClient = class {
|
|
|
1193
1225
|
/**
|
|
1194
1226
|
* GET request
|
|
1195
1227
|
*/
|
|
1196
|
-
async get(
|
|
1197
|
-
return this.request("GET",
|
|
1228
|
+
async get(path9) {
|
|
1229
|
+
return this.request("GET", path9);
|
|
1198
1230
|
}
|
|
1199
1231
|
/**
|
|
1200
1232
|
* PATCH request
|
|
1201
1233
|
*/
|
|
1202
|
-
async patch(
|
|
1203
|
-
return this.request("PATCH",
|
|
1234
|
+
async patch(path9, body) {
|
|
1235
|
+
return this.request("PATCH", path9, body);
|
|
1204
1236
|
}
|
|
1205
1237
|
/**
|
|
1206
1238
|
* PUT request
|
|
1207
1239
|
*/
|
|
1208
|
-
async put(
|
|
1209
|
-
return this.request("PUT",
|
|
1240
|
+
async put(path9, body) {
|
|
1241
|
+
return this.request("PUT", path9, body);
|
|
1210
1242
|
}
|
|
1211
1243
|
/**
|
|
1212
1244
|
* Make an HTTP request to Firecracker API
|
|
1213
1245
|
*
|
|
1214
1246
|
* @param timeoutMs Request timeout in milliseconds (default: 30000ms)
|
|
1215
1247
|
*/
|
|
1216
|
-
request(method,
|
|
1248
|
+
request(method, path9, body, timeoutMs = 3e4) {
|
|
1217
1249
|
return new Promise((resolve, reject) => {
|
|
1218
1250
|
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
1219
1251
|
const headers = {
|
|
@@ -1227,7 +1259,7 @@ var FirecrackerClient = class {
|
|
|
1227
1259
|
}
|
|
1228
1260
|
const options = {
|
|
1229
1261
|
socketPath: this.socketPath,
|
|
1230
|
-
path:
|
|
1262
|
+
path: path9,
|
|
1231
1263
|
method,
|
|
1232
1264
|
headers,
|
|
1233
1265
|
timeout: timeoutMs,
|
|
@@ -1235,7 +1267,7 @@ var FirecrackerClient = class {
|
|
|
1235
1267
|
// Firecracker's single-threaded API can have issues with pipelined requests
|
|
1236
1268
|
agent: false
|
|
1237
1269
|
};
|
|
1238
|
-
logger3.log(`${method} ${
|
|
1270
|
+
logger3.log(`${method} ${path9}${bodyStr ? " " + bodyStr : ""}`);
|
|
1239
1271
|
const req = http.request(options, (res) => {
|
|
1240
1272
|
let data = "";
|
|
1241
1273
|
res.on("data", (chunk) => {
|
|
@@ -1252,14 +1284,14 @@ var FirecrackerClient = class {
|
|
|
1252
1284
|
faultMessage = errorBody.fault_message || data;
|
|
1253
1285
|
} catch {
|
|
1254
1286
|
}
|
|
1255
|
-
reject(new FirecrackerApiError(statusCode,
|
|
1287
|
+
reject(new FirecrackerApiError(statusCode, path9, faultMessage));
|
|
1256
1288
|
}
|
|
1257
1289
|
});
|
|
1258
1290
|
});
|
|
1259
1291
|
req.on("timeout", () => {
|
|
1260
1292
|
req.destroy();
|
|
1261
1293
|
reject(
|
|
1262
|
-
new Error(`Request timeout after ${timeoutMs}ms: ${method} ${
|
|
1294
|
+
new Error(`Request timeout after ${timeoutMs}ms: ${method} ${path9}`)
|
|
1263
1295
|
);
|
|
1264
1296
|
});
|
|
1265
1297
|
req.on("error", (err) => {
|
|
@@ -1352,7 +1384,7 @@ var FirecrackerVM = class {
|
|
|
1352
1384
|
this.workDir = config.workDir;
|
|
1353
1385
|
this.vsockPath = vmPaths.vsock(this.workDir);
|
|
1354
1386
|
this.configPath = vmPaths.config(this.workDir);
|
|
1355
|
-
this.apiSocketPath =
|
|
1387
|
+
this.apiSocketPath = vmPaths.apiSock(this.workDir);
|
|
1356
1388
|
}
|
|
1357
1389
|
/**
|
|
1358
1390
|
* Get current VM state
|
|
@@ -1451,6 +1483,7 @@ var FirecrackerVM = class {
|
|
|
1451
1483
|
const config = this.buildConfig();
|
|
1452
1484
|
fs4.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
1453
1485
|
logger4.log(`[VM ${this.config.vmId}] Starting Firecracker (fresh boot)...`);
|
|
1486
|
+
const currentUser = os.userInfo().username;
|
|
1454
1487
|
this.process = spawn(
|
|
1455
1488
|
"sudo",
|
|
1456
1489
|
[
|
|
@@ -1458,6 +1491,9 @@ var FirecrackerVM = class {
|
|
|
1458
1491
|
"netns",
|
|
1459
1492
|
"exec",
|
|
1460
1493
|
this.netns.name,
|
|
1494
|
+
"sudo",
|
|
1495
|
+
"-u",
|
|
1496
|
+
currentUser,
|
|
1461
1497
|
this.config.firecrackerBinary,
|
|
1462
1498
|
"--config-file",
|
|
1463
1499
|
this.configPath,
|
|
@@ -1475,25 +1511,58 @@ var FirecrackerVM = class {
|
|
|
1475
1511
|
* Start VM from snapshot
|
|
1476
1512
|
* Uses --api-sock to load snapshot via API
|
|
1477
1513
|
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1514
|
+
* Snapshot contains original absolute paths for drives. We use mount namespace
|
|
1515
|
+
* isolation to bind mount our actual overlay file to the path expected by the snapshot.
|
|
1516
|
+
* This allows concurrent VMs to each have their own overlay while restoring from
|
|
1517
|
+
* the same snapshot.
|
|
1480
1518
|
*/
|
|
1481
1519
|
async startFromSnapshot(snapshot) {
|
|
1482
1520
|
logger4.log(
|
|
1483
1521
|
`[VM ${this.config.vmId}] Starting Firecracker (snapshot restore)...`
|
|
1484
1522
|
);
|
|
1485
|
-
logger4.log(`[VM ${this.config.vmId}] Snapshot: ${snapshot.
|
|
1486
|
-
logger4.log(`[VM ${this.config.vmId}] Memory: ${snapshot.
|
|
1523
|
+
logger4.log(`[VM ${this.config.vmId}] Snapshot: ${snapshot.snapshot}`);
|
|
1524
|
+
logger4.log(`[VM ${this.config.vmId}] Memory: ${snapshot.memory}`);
|
|
1525
|
+
const actualVsockDir = vmPaths.vsockDir(this.workDir);
|
|
1526
|
+
logger4.log(
|
|
1527
|
+
`[VM ${this.config.vmId}] Snapshot vsock: ${snapshot.snapshotVsockDir}`
|
|
1528
|
+
);
|
|
1529
|
+
logger4.log(
|
|
1530
|
+
`[VM ${this.config.vmId}] Snapshot overlay: ${snapshot.snapshotOverlay}`
|
|
1531
|
+
);
|
|
1532
|
+
logger4.log(`[VM ${this.config.vmId}] Actual vsock: ${actualVsockDir}`);
|
|
1533
|
+
logger4.log(
|
|
1534
|
+
`[VM ${this.config.vmId}] Actual overlay: ${this.vmOverlayPath}`
|
|
1535
|
+
);
|
|
1536
|
+
fs4.mkdirSync(snapshot.snapshotVsockDir, { recursive: true });
|
|
1537
|
+
fs4.mkdirSync(path4.dirname(snapshot.snapshotOverlay), {
|
|
1538
|
+
recursive: true
|
|
1539
|
+
});
|
|
1540
|
+
if (!fs4.existsSync(snapshot.snapshotOverlay)) {
|
|
1541
|
+
fs4.writeFileSync(snapshot.snapshotOverlay, "");
|
|
1542
|
+
}
|
|
1543
|
+
const currentUser = os.userInfo().username;
|
|
1544
|
+
const bindMountVsock = `mount --bind "${actualVsockDir}" "${snapshot.snapshotVsockDir}"`;
|
|
1545
|
+
const bindMountOverlay = `mount --bind "${this.vmOverlayPath}" "${snapshot.snapshotOverlay}"`;
|
|
1546
|
+
const firecrackerCmd = [
|
|
1547
|
+
"ip",
|
|
1548
|
+
"netns",
|
|
1549
|
+
"exec",
|
|
1550
|
+
this.netns.name,
|
|
1551
|
+
"sudo",
|
|
1552
|
+
"-u",
|
|
1553
|
+
currentUser,
|
|
1554
|
+
this.config.firecrackerBinary,
|
|
1555
|
+
"--api-sock",
|
|
1556
|
+
this.apiSocketPath
|
|
1557
|
+
].join(" ");
|
|
1487
1558
|
this.process = spawn(
|
|
1488
1559
|
"sudo",
|
|
1489
1560
|
[
|
|
1490
|
-
"
|
|
1491
|
-
"
|
|
1492
|
-
"
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
"--api-sock",
|
|
1496
|
-
this.apiSocketPath
|
|
1561
|
+
"unshare",
|
|
1562
|
+
"--mount",
|
|
1563
|
+
"bash",
|
|
1564
|
+
"-c",
|
|
1565
|
+
`${bindMountVsock} && ${bindMountOverlay} && ${firecrackerCmd}`
|
|
1497
1566
|
],
|
|
1498
1567
|
{
|
|
1499
1568
|
cwd: this.workDir,
|
|
@@ -1504,26 +1573,11 @@ var FirecrackerVM = class {
|
|
|
1504
1573
|
this.setupProcessHandlers();
|
|
1505
1574
|
const client = new FirecrackerClient(this.apiSocketPath);
|
|
1506
1575
|
await this.waitForApiReady(client);
|
|
1507
|
-
logger4.log(`[VM ${this.config.vmId}] Configuring drives...`);
|
|
1508
|
-
await Promise.all([
|
|
1509
|
-
client.configureDrive({
|
|
1510
|
-
drive_id: "rootfs",
|
|
1511
|
-
path_on_host: this.config.rootfsPath,
|
|
1512
|
-
is_root_device: true,
|
|
1513
|
-
is_read_only: true
|
|
1514
|
-
}),
|
|
1515
|
-
client.configureDrive({
|
|
1516
|
-
drive_id: "overlay",
|
|
1517
|
-
path_on_host: this.vmOverlayPath,
|
|
1518
|
-
is_root_device: false,
|
|
1519
|
-
is_read_only: false
|
|
1520
|
-
})
|
|
1521
|
-
]);
|
|
1522
1576
|
logger4.log(`[VM ${this.config.vmId}] Loading snapshot...`);
|
|
1523
1577
|
await client.loadSnapshot({
|
|
1524
|
-
snapshot_path: snapshot.
|
|
1578
|
+
snapshot_path: snapshot.snapshot,
|
|
1525
1579
|
mem_backend: {
|
|
1526
|
-
backend_path: snapshot.
|
|
1580
|
+
backend_path: snapshot.memory,
|
|
1527
1581
|
backend_type: "File"
|
|
1528
1582
|
},
|
|
1529
1583
|
resume_vm: true
|
|
@@ -1756,8 +1810,8 @@ function encodeExecPayload(command, timeoutMs) {
|
|
|
1756
1810
|
cmdBuf.copy(payload, 8);
|
|
1757
1811
|
return payload;
|
|
1758
1812
|
}
|
|
1759
|
-
function encodeWriteFilePayload(
|
|
1760
|
-
const pathBuf = Buffer.from(
|
|
1813
|
+
function encodeWriteFilePayload(path9, content, sudo) {
|
|
1814
|
+
const pathBuf = Buffer.from(path9, "utf-8");
|
|
1761
1815
|
if (pathBuf.length > 65535) {
|
|
1762
1816
|
throw new Error(`Path too long: ${pathBuf.length} bytes (max 65535)`);
|
|
1763
1817
|
}
|
|
@@ -2652,8 +2706,8 @@ function getErrorMap() {
|
|
|
2652
2706
|
return overrideErrorMap;
|
|
2653
2707
|
}
|
|
2654
2708
|
var makeIssue = (params) => {
|
|
2655
|
-
const { data, path:
|
|
2656
|
-
const fullPath = [...
|
|
2709
|
+
const { data, path: path9, errorMaps, issueData } = params;
|
|
2710
|
+
const fullPath = [...path9, ...issueData.path || []];
|
|
2657
2711
|
const fullIssue = {
|
|
2658
2712
|
...issueData,
|
|
2659
2713
|
path: fullPath
|
|
@@ -2752,11 +2806,11 @@ var errorUtil;
|
|
|
2752
2806
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message === null || message === void 0 ? void 0 : message.message;
|
|
2753
2807
|
})(errorUtil || (errorUtil = {}));
|
|
2754
2808
|
var ParseInputLazyPath = class {
|
|
2755
|
-
constructor(parent, value,
|
|
2809
|
+
constructor(parent, value, path9, key) {
|
|
2756
2810
|
this._cachedPath = [];
|
|
2757
2811
|
this.parent = parent;
|
|
2758
2812
|
this.data = value;
|
|
2759
|
-
this._path =
|
|
2813
|
+
this._path = path9;
|
|
2760
2814
|
this._key = key;
|
|
2761
2815
|
}
|
|
2762
2816
|
get path() {
|
|
@@ -9220,7 +9274,7 @@ function initVMRegistry(registryPath) {
|
|
|
9220
9274
|
// src/lib/proxy/proxy-manager.ts
|
|
9221
9275
|
import { spawn as spawn2 } from "child_process";
|
|
9222
9276
|
import fs7 from "fs";
|
|
9223
|
-
import
|
|
9277
|
+
import path5 from "path";
|
|
9224
9278
|
|
|
9225
9279
|
// src/lib/proxy/mitm-addon-script.ts
|
|
9226
9280
|
var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
|
|
@@ -9716,7 +9770,7 @@ var ProxyManager = class {
|
|
|
9716
9770
|
process = null;
|
|
9717
9771
|
isRunning = false;
|
|
9718
9772
|
constructor(config) {
|
|
9719
|
-
const addonPath =
|
|
9773
|
+
const addonPath = path5.join(config.caDir, "mitm_addon.py");
|
|
9720
9774
|
this.config = {
|
|
9721
9775
|
...DEFAULT_PROXY_OPTIONS,
|
|
9722
9776
|
...config,
|
|
@@ -9743,7 +9797,7 @@ var ProxyManager = class {
|
|
|
9743
9797
|
* Ensure the addon script exists at the configured path
|
|
9744
9798
|
*/
|
|
9745
9799
|
ensureAddonScript() {
|
|
9746
|
-
const addonDir =
|
|
9800
|
+
const addonDir = path5.dirname(this.config.addonPath);
|
|
9747
9801
|
if (!fs7.existsSync(addonDir)) {
|
|
9748
9802
|
fs7.mkdirSync(addonDir, { recursive: true });
|
|
9749
9803
|
}
|
|
@@ -9759,7 +9813,7 @@ var ProxyManager = class {
|
|
|
9759
9813
|
if (!fs7.existsSync(this.config.caDir)) {
|
|
9760
9814
|
throw new Error(`Proxy CA directory not found: ${this.config.caDir}`);
|
|
9761
9815
|
}
|
|
9762
|
-
const caCertPath =
|
|
9816
|
+
const caCertPath = path5.join(this.config.caDir, "mitmproxy-ca.pem");
|
|
9763
9817
|
if (!fs7.existsSync(caCertPath)) {
|
|
9764
9818
|
throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
|
|
9765
9819
|
}
|
|
@@ -10212,7 +10266,21 @@ async function executeJob(context, config, options = {}) {
|
|
|
10212
10266
|
const guestConnectionPromise = guest.waitForGuestConnection(3e4);
|
|
10213
10267
|
logger9.log(`Creating VM ${vmId}...`);
|
|
10214
10268
|
vm = new FirecrackerVM(vmConfig);
|
|
10215
|
-
|
|
10269
|
+
const snapshotConfig = config.firecracker.snapshot;
|
|
10270
|
+
let snapshotPaths;
|
|
10271
|
+
if (snapshotConfig) {
|
|
10272
|
+
const snapshotDir = path6.dirname(snapshotConfig.snapshot);
|
|
10273
|
+
const originalBaseDir = path6.dirname(snapshotDir);
|
|
10274
|
+
const snapshotBaseDir = runnerPaths.snapshotBaseDir(originalBaseDir);
|
|
10275
|
+
const snapshotWorkDir = runnerPaths.snapshotWorkDir(snapshotBaseDir);
|
|
10276
|
+
snapshotPaths = {
|
|
10277
|
+
snapshot: snapshotConfig.snapshot,
|
|
10278
|
+
memory: snapshotConfig.memory,
|
|
10279
|
+
snapshotOverlay: vmPaths.overlay(snapshotWorkDir),
|
|
10280
|
+
snapshotVsockDir: vmPaths.vsockDir(snapshotWorkDir)
|
|
10281
|
+
};
|
|
10282
|
+
}
|
|
10283
|
+
await withSandboxTiming("vm_create", () => vm.start(snapshotPaths));
|
|
10216
10284
|
guestIp = vm.getGuestIp();
|
|
10217
10285
|
vethNsIp = vm.getNetns()?.vethNsIp ?? null;
|
|
10218
10286
|
if (!guestIp || !vethNsIp) {
|
|
@@ -10240,6 +10308,10 @@ async function executeJob(context, config, options = {}) {
|
|
|
10240
10308
|
logger9.log(`Waiting for guest connection...`);
|
|
10241
10309
|
await withSandboxTiming("guest_wait", () => guestConnectionPromise);
|
|
10242
10310
|
logger9.log(`Guest client ready`);
|
|
10311
|
+
if (config.firecracker.snapshot) {
|
|
10312
|
+
const timestamp = (Date.now() / 1e3).toFixed(3);
|
|
10313
|
+
await guest.exec(`date -s "@${timestamp}"`);
|
|
10314
|
+
}
|
|
10243
10315
|
if (context.storageManifest) {
|
|
10244
10316
|
await withSandboxTiming(
|
|
10245
10317
|
"storage_download",
|
|
@@ -10442,7 +10514,7 @@ async function isPortInUse(port) {
|
|
|
10442
10514
|
|
|
10443
10515
|
// src/lib/runner/runner-lock.ts
|
|
10444
10516
|
import fs10 from "fs";
|
|
10445
|
-
import
|
|
10517
|
+
import path7 from "path";
|
|
10446
10518
|
var logger11 = createLogger("RunnerLock");
|
|
10447
10519
|
var DEFAULT_PID_FILE = runtimePaths.runnerPid;
|
|
10448
10520
|
var currentPidFile = null;
|
|
@@ -10459,7 +10531,7 @@ function isProcessRunning(pid) {
|
|
|
10459
10531
|
}
|
|
10460
10532
|
function acquireRunnerLock(options = {}) {
|
|
10461
10533
|
const pidFile = options.pidFile ?? DEFAULT_PID_FILE;
|
|
10462
|
-
const runDir =
|
|
10534
|
+
const runDir = path7.dirname(pidFile);
|
|
10463
10535
|
fs10.mkdirSync(runDir, { recursive: true });
|
|
10464
10536
|
if (fs10.existsSync(pidFile)) {
|
|
10465
10537
|
const pidStr = fs10.readFileSync(pidFile, "utf-8").trim();
|
|
@@ -10493,7 +10565,7 @@ function releaseRunnerLock() {
|
|
|
10493
10565
|
var logger12 = createLogger("Runner");
|
|
10494
10566
|
async function setupEnvironment(options) {
|
|
10495
10567
|
const { config } = options;
|
|
10496
|
-
|
|
10568
|
+
acquireRunnerLock();
|
|
10497
10569
|
const networkCheck = checkNetworkPrerequisites();
|
|
10498
10570
|
if (!networkCheck.ok) {
|
|
10499
10571
|
logger12.error("Network prerequisites not met:");
|
|
@@ -10523,10 +10595,16 @@ async function setupEnvironment(options) {
|
|
|
10523
10595
|
);
|
|
10524
10596
|
}
|
|
10525
10597
|
logger12.log("Initializing overlay pool...");
|
|
10598
|
+
const snapshotConfig = config.firecracker.snapshot;
|
|
10526
10599
|
await initOverlayPool({
|
|
10527
10600
|
size: config.sandbox.max_concurrent + 2,
|
|
10528
10601
|
replenishThreshold: config.sandbox.max_concurrent,
|
|
10529
|
-
poolDir: runnerPaths.overlayPool(config.base_dir)
|
|
10602
|
+
poolDir: runnerPaths.overlayPool(config.base_dir),
|
|
10603
|
+
createFile: snapshotConfig ? (filePath) => execCommand(
|
|
10604
|
+
`cp --sparse=always "${snapshotConfig.overlay}" "${filePath}"`,
|
|
10605
|
+
false
|
|
10606
|
+
).then(() => {
|
|
10607
|
+
}) : void 0
|
|
10530
10608
|
});
|
|
10531
10609
|
logger12.log("Initializing namespace pool...");
|
|
10532
10610
|
await initNetnsPool({
|
|
@@ -10834,7 +10912,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync a
|
|
|
10834
10912
|
|
|
10835
10913
|
// src/lib/firecracker/process.ts
|
|
10836
10914
|
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
|
|
10837
|
-
import
|
|
10915
|
+
import path8 from "path";
|
|
10838
10916
|
function parseFirecrackerCmdline(cmdline) {
|
|
10839
10917
|
const args = cmdline.split("\0");
|
|
10840
10918
|
if (!args[0]?.includes("firecracker")) return null;
|
|
@@ -10867,7 +10945,7 @@ function findFirecrackerProcesses() {
|
|
|
10867
10945
|
for (const entry of entries) {
|
|
10868
10946
|
if (!/^\d+$/.test(entry)) continue;
|
|
10869
10947
|
const pid = parseInt(entry, 10);
|
|
10870
|
-
const cmdlinePath =
|
|
10948
|
+
const cmdlinePath = path8.join(procDir, entry, "cmdline");
|
|
10871
10949
|
if (!existsSync4(cmdlinePath)) continue;
|
|
10872
10950
|
try {
|
|
10873
10951
|
const cmdline = readFileSync2(cmdlinePath, "utf-8");
|
|
@@ -10925,7 +11003,7 @@ function findMitmproxyProcess() {
|
|
|
10925
11003
|
for (const entry of entries) {
|
|
10926
11004
|
if (!/^\d+$/.test(entry)) continue;
|
|
10927
11005
|
const pid = parseInt(entry, 10);
|
|
10928
|
-
const cmdlinePath =
|
|
11006
|
+
const cmdlinePath = path8.join(procDir, entry, "cmdline");
|
|
10929
11007
|
if (!existsSync4(cmdlinePath)) continue;
|
|
10930
11008
|
try {
|
|
10931
11009
|
const cmdline = readFileSync2(cmdlinePath, "utf-8");
|
|
@@ -10941,142 +11019,127 @@ function findMitmproxyProcess() {
|
|
|
10941
11019
|
}
|
|
10942
11020
|
|
|
10943
11021
|
// src/commands/doctor.ts
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
console.log(`Mode: ${status.mode}`);
|
|
10959
|
-
if (status.started_at) {
|
|
10960
|
-
const started = new Date(status.started_at);
|
|
10961
|
-
const uptime = formatUptime(Date.now() - started.getTime());
|
|
10962
|
-
console.log(
|
|
10963
|
-
`Started: ${started.toLocaleString()} (uptime: ${uptime})`
|
|
10964
|
-
);
|
|
10965
|
-
}
|
|
10966
|
-
} catch {
|
|
10967
|
-
console.log("Mode: unknown (status.json unreadable)");
|
|
10968
|
-
}
|
|
10969
|
-
} else {
|
|
10970
|
-
console.log("Mode: unknown (no status.json)");
|
|
10971
|
-
}
|
|
10972
|
-
console.log("");
|
|
10973
|
-
console.log("API Connectivity:");
|
|
10974
|
-
try {
|
|
10975
|
-
await pollForJob(config.server, config.group);
|
|
10976
|
-
console.log(` \u2713 Connected to ${config.server.url}`);
|
|
10977
|
-
console.log(" \u2713 Authentication: OK");
|
|
10978
|
-
} catch (error) {
|
|
10979
|
-
console.log(` \u2717 Cannot connect to ${config.server.url}`);
|
|
10980
|
-
console.log(
|
|
10981
|
-
` Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10982
|
-
);
|
|
10983
|
-
}
|
|
10984
|
-
console.log("");
|
|
10985
|
-
console.log("Network:");
|
|
10986
|
-
const warnings = [];
|
|
10987
|
-
const proxyPort = config.proxy.port;
|
|
10988
|
-
const mitmProc = findMitmproxyProcess();
|
|
10989
|
-
const portInUse = await isPortInUse(proxyPort);
|
|
10990
|
-
if (mitmProc) {
|
|
10991
|
-
console.log(
|
|
10992
|
-
` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`
|
|
10993
|
-
);
|
|
10994
|
-
} else if (portInUse) {
|
|
10995
|
-
console.log(
|
|
10996
|
-
` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
|
|
10997
|
-
);
|
|
10998
|
-
warnings.push({
|
|
10999
|
-
message: `Port ${proxyPort} is in use but mitmproxy process not detected`
|
|
11000
|
-
});
|
|
11001
|
-
} else {
|
|
11002
|
-
console.log(` \u2717 Proxy mitmproxy not running`);
|
|
11003
|
-
warnings.push({ message: "Proxy mitmproxy is not running" });
|
|
11004
|
-
}
|
|
11005
|
-
console.log(
|
|
11006
|
-
` \u2139 Namespaces: each VM runs in isolated namespace with IP ${SNAPSHOT_NETWORK.guestIp}`
|
|
11007
|
-
);
|
|
11008
|
-
console.log("");
|
|
11009
|
-
const processes = findFirecrackerProcesses();
|
|
11010
|
-
const workspaces = existsSync5(workspacesDir) ? readdirSync2(workspacesDir).filter(runnerPaths.isVmWorkspace) : [];
|
|
11011
|
-
const jobs = [];
|
|
11012
|
-
const statusVmIds = /* @__PURE__ */ new Set();
|
|
11013
|
-
if (status?.active_run_ids) {
|
|
11014
|
-
for (const runId of status.active_run_ids) {
|
|
11015
|
-
const vmId = createVmId(runId);
|
|
11016
|
-
statusVmIds.add(vmId);
|
|
11017
|
-
const proc = processes.find((p) => p.vmId === vmId);
|
|
11018
|
-
jobs.push({
|
|
11019
|
-
runId,
|
|
11020
|
-
vmId,
|
|
11021
|
-
hasProcess: !!proc,
|
|
11022
|
-
pid: proc?.pid
|
|
11023
|
-
});
|
|
11024
|
-
}
|
|
11025
|
-
}
|
|
11026
|
-
const maxConcurrent = config.sandbox.max_concurrent;
|
|
11027
|
-
console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
|
|
11028
|
-
if (jobs.length === 0) {
|
|
11029
|
-
console.log(" No active runs");
|
|
11030
|
-
} else {
|
|
11031
|
-
console.log(
|
|
11032
|
-
" Run ID VM ID Status"
|
|
11033
|
-
);
|
|
11034
|
-
for (const job of jobs) {
|
|
11035
|
-
const statusText = job.hasProcess ? `\u2713 Running (PID ${job.pid})` : "\u26A0\uFE0F No process";
|
|
11036
|
-
console.log(` ${job.runId} ${job.vmId} ${statusText}`);
|
|
11037
|
-
}
|
|
11038
|
-
}
|
|
11039
|
-
console.log("");
|
|
11040
|
-
for (const job of jobs) {
|
|
11041
|
-
if (!job.hasProcess) {
|
|
11042
|
-
warnings.push({
|
|
11043
|
-
message: `Run ${job.vmId} in status.json but no Firecracker process running`
|
|
11044
|
-
});
|
|
11045
|
-
}
|
|
11046
|
-
}
|
|
11047
|
-
const processVmIds = new Set(processes.map((p) => p.vmId));
|
|
11048
|
-
for (const proc of processes) {
|
|
11049
|
-
if (!statusVmIds.has(proc.vmId)) {
|
|
11050
|
-
warnings.push({
|
|
11051
|
-
message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
|
|
11052
|
-
});
|
|
11053
|
-
}
|
|
11054
|
-
}
|
|
11055
|
-
for (const ws of workspaces) {
|
|
11056
|
-
const vmId = runnerPaths.extractVmId(ws);
|
|
11057
|
-
if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
|
|
11058
|
-
warnings.push({
|
|
11059
|
-
message: `Orphan workspace: ${ws} (no matching job or process)`
|
|
11060
|
-
});
|
|
11061
|
-
}
|
|
11062
|
-
}
|
|
11063
|
-
console.log("Warnings:");
|
|
11064
|
-
if (warnings.length === 0) {
|
|
11065
|
-
console.log(" None");
|
|
11066
|
-
} else {
|
|
11067
|
-
for (const w of warnings) {
|
|
11068
|
-
console.log(` - ${w.message}`);
|
|
11069
|
-
}
|
|
11070
|
-
}
|
|
11071
|
-
process.exit(warnings.length > 0 ? 1 : 0);
|
|
11072
|
-
} catch (error) {
|
|
11073
|
-
console.error(
|
|
11074
|
-
`Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11075
|
-
);
|
|
11076
|
-
process.exit(1);
|
|
11022
|
+
function displayRunnerStatus(statusFilePath) {
|
|
11023
|
+
if (!existsSync5(statusFilePath)) {
|
|
11024
|
+
console.log("Mode: unknown (no status.json)");
|
|
11025
|
+
return null;
|
|
11026
|
+
}
|
|
11027
|
+
try {
|
|
11028
|
+
const status = JSON.parse(
|
|
11029
|
+
readFileSync3(statusFilePath, "utf-8")
|
|
11030
|
+
);
|
|
11031
|
+
console.log(`Mode: ${status.mode}`);
|
|
11032
|
+
if (status.started_at) {
|
|
11033
|
+
const started = new Date(status.started_at);
|
|
11034
|
+
const uptime = formatUptime(Date.now() - started.getTime());
|
|
11035
|
+
console.log(`Started: ${started.toLocaleString()} (uptime: ${uptime})`);
|
|
11077
11036
|
}
|
|
11037
|
+
return status;
|
|
11038
|
+
} catch {
|
|
11039
|
+
console.log("Mode: unknown (status.json unreadable)");
|
|
11040
|
+
return null;
|
|
11078
11041
|
}
|
|
11079
|
-
|
|
11042
|
+
}
|
|
11043
|
+
async function checkApiConnectivity(config) {
|
|
11044
|
+
console.log("API Connectivity:");
|
|
11045
|
+
try {
|
|
11046
|
+
await pollForJob(config.server, config.group);
|
|
11047
|
+
console.log(` \u2713 Connected to ${config.server.url}`);
|
|
11048
|
+
console.log(" \u2713 Authentication: OK");
|
|
11049
|
+
} catch (error) {
|
|
11050
|
+
console.log(` \u2717 Cannot connect to ${config.server.url}`);
|
|
11051
|
+
console.log(
|
|
11052
|
+
` Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11053
|
+
);
|
|
11054
|
+
}
|
|
11055
|
+
}
|
|
11056
|
+
async function checkNetwork(config, warnings) {
|
|
11057
|
+
console.log("Network:");
|
|
11058
|
+
const proxyPort = config.proxy.port;
|
|
11059
|
+
const mitmProc = findMitmproxyProcess();
|
|
11060
|
+
const portInUse = await isPortInUse(proxyPort);
|
|
11061
|
+
if (mitmProc) {
|
|
11062
|
+
console.log(` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`);
|
|
11063
|
+
} else if (portInUse) {
|
|
11064
|
+
console.log(
|
|
11065
|
+
` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
|
|
11066
|
+
);
|
|
11067
|
+
warnings.push({
|
|
11068
|
+
message: `Port ${proxyPort} is in use but mitmproxy process not detected`
|
|
11069
|
+
});
|
|
11070
|
+
} else {
|
|
11071
|
+
console.log(` \u2717 Proxy mitmproxy not running`);
|
|
11072
|
+
warnings.push({ message: "Proxy mitmproxy is not running" });
|
|
11073
|
+
}
|
|
11074
|
+
console.log(
|
|
11075
|
+
` \u2139 Namespaces: each VM runs in isolated namespace with IP ${SNAPSHOT_NETWORK.guestIp}`
|
|
11076
|
+
);
|
|
11077
|
+
}
|
|
11078
|
+
function buildJobInfo(status, processes) {
|
|
11079
|
+
const jobs = [];
|
|
11080
|
+
const statusVmIds = /* @__PURE__ */ new Set();
|
|
11081
|
+
if (status?.active_run_ids) {
|
|
11082
|
+
for (const runId of status.active_run_ids) {
|
|
11083
|
+
const vmId = createVmId(runId);
|
|
11084
|
+
statusVmIds.add(vmId);
|
|
11085
|
+
const proc = processes.find((p) => p.vmId === vmId);
|
|
11086
|
+
jobs.push({
|
|
11087
|
+
runId,
|
|
11088
|
+
vmId,
|
|
11089
|
+
hasProcess: !!proc,
|
|
11090
|
+
pid: proc?.pid
|
|
11091
|
+
});
|
|
11092
|
+
}
|
|
11093
|
+
}
|
|
11094
|
+
return { jobs, statusVmIds };
|
|
11095
|
+
}
|
|
11096
|
+
function displayRuns(jobs, maxConcurrent) {
|
|
11097
|
+
console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
|
|
11098
|
+
if (jobs.length === 0) {
|
|
11099
|
+
console.log(" No active runs");
|
|
11100
|
+
return;
|
|
11101
|
+
}
|
|
11102
|
+
console.log(" Run ID VM ID Status");
|
|
11103
|
+
for (const job of jobs) {
|
|
11104
|
+
const statusText = job.hasProcess ? `\u2713 Running (PID ${job.pid})` : "\u26A0\uFE0F No process";
|
|
11105
|
+
console.log(` ${job.runId} ${job.vmId} ${statusText}`);
|
|
11106
|
+
}
|
|
11107
|
+
}
|
|
11108
|
+
function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings) {
|
|
11109
|
+
for (const job of jobs) {
|
|
11110
|
+
if (!job.hasProcess) {
|
|
11111
|
+
warnings.push({
|
|
11112
|
+
message: `Run ${job.vmId} in status.json but no Firecracker process running`
|
|
11113
|
+
});
|
|
11114
|
+
}
|
|
11115
|
+
}
|
|
11116
|
+
const processVmIds = new Set(processes.map((p) => p.vmId));
|
|
11117
|
+
for (const proc of processes) {
|
|
11118
|
+
if (!statusVmIds.has(proc.vmId)) {
|
|
11119
|
+
warnings.push({
|
|
11120
|
+
message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
|
|
11121
|
+
});
|
|
11122
|
+
}
|
|
11123
|
+
}
|
|
11124
|
+
for (const ws of workspaces) {
|
|
11125
|
+
const vmId = runnerPaths.extractVmId(ws);
|
|
11126
|
+
if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
|
|
11127
|
+
warnings.push({
|
|
11128
|
+
message: `Orphan workspace: ${ws} (no matching job or process)`
|
|
11129
|
+
});
|
|
11130
|
+
}
|
|
11131
|
+
}
|
|
11132
|
+
}
|
|
11133
|
+
function displayWarnings(warnings) {
|
|
11134
|
+
console.log("Warnings:");
|
|
11135
|
+
if (warnings.length === 0) {
|
|
11136
|
+
console.log(" None");
|
|
11137
|
+
} else {
|
|
11138
|
+
for (const w of warnings) {
|
|
11139
|
+
console.log(` - ${w.message}`);
|
|
11140
|
+
}
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11080
11143
|
function formatUptime(ms) {
|
|
11081
11144
|
const seconds = Math.floor(ms / 1e3);
|
|
11082
11145
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -11087,6 +11150,34 @@ function formatUptime(ms) {
|
|
|
11087
11150
|
if (minutes > 0) return `${minutes}m`;
|
|
11088
11151
|
return `${seconds}s`;
|
|
11089
11152
|
}
|
|
11153
|
+
var doctorCommand = new Command2("doctor").description("Diagnose runner health, check network, and detect issues").option("--config <path>", "Config file path", "./runner.yaml").action(async (options) => {
|
|
11154
|
+
try {
|
|
11155
|
+
const config = loadConfig(options.config);
|
|
11156
|
+
const statusFilePath = runnerPaths.statusFile(config.base_dir);
|
|
11157
|
+
const workspacesDir = runnerPaths.workspacesDir(config.base_dir);
|
|
11158
|
+
const warnings = [];
|
|
11159
|
+
console.log(`Runner: ${config.name}`);
|
|
11160
|
+
const status = displayRunnerStatus(statusFilePath);
|
|
11161
|
+
console.log("");
|
|
11162
|
+
await checkApiConnectivity(config);
|
|
11163
|
+
console.log("");
|
|
11164
|
+
await checkNetwork(config, warnings);
|
|
11165
|
+
console.log("");
|
|
11166
|
+
const processes = findFirecrackerProcesses();
|
|
11167
|
+
const workspaces = existsSync5(workspacesDir) ? readdirSync2(workspacesDir).filter(runnerPaths.isVmWorkspace) : [];
|
|
11168
|
+
const { jobs, statusVmIds } = buildJobInfo(status, processes);
|
|
11169
|
+
displayRuns(jobs, config.sandbox.max_concurrent);
|
|
11170
|
+
console.log("");
|
|
11171
|
+
detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings);
|
|
11172
|
+
displayWarnings(warnings);
|
|
11173
|
+
process.exit(warnings.length > 0 ? 1 : 0);
|
|
11174
|
+
} catch (error) {
|
|
11175
|
+
console.error(
|
|
11176
|
+
`Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11177
|
+
);
|
|
11178
|
+
process.exit(1);
|
|
11179
|
+
}
|
|
11180
|
+
});
|
|
11090
11181
|
|
|
11091
11182
|
// src/commands/kill.ts
|
|
11092
11183
|
import { Command as Command3 } from "commander";
|
|
@@ -11323,10 +11414,16 @@ var benchmarkCommand = new Command4("benchmark").description(
|
|
|
11323
11414
|
process.exit(1);
|
|
11324
11415
|
}
|
|
11325
11416
|
timer.log("Initializing pools...");
|
|
11417
|
+
const snapshotConfig = config.firecracker.snapshot;
|
|
11326
11418
|
await initOverlayPool({
|
|
11327
11419
|
size: 2,
|
|
11328
11420
|
replenishThreshold: 1,
|
|
11329
|
-
poolDir: runnerPaths.overlayPool(config.base_dir)
|
|
11421
|
+
poolDir: runnerPaths.overlayPool(config.base_dir),
|
|
11422
|
+
createFile: snapshotConfig ? (filePath) => execCommand(
|
|
11423
|
+
`cp --sparse=always "${snapshotConfig.overlay}" "${filePath}"`,
|
|
11424
|
+
false
|
|
11425
|
+
).then(() => {
|
|
11426
|
+
}) : void 0
|
|
11330
11427
|
});
|
|
11331
11428
|
await initNetnsPool({ name: config.name, size: 2 });
|
|
11332
11429
|
poolsInitialized = true;
|
|
@@ -11354,12 +11451,219 @@ var benchmarkCommand = new Command4("benchmark").description(
|
|
|
11354
11451
|
process.exit(exitCode);
|
|
11355
11452
|
});
|
|
11356
11453
|
|
|
11454
|
+
// src/commands/snapshot.ts
|
|
11455
|
+
import { Command as Command5 } from "commander";
|
|
11456
|
+
import { spawn as spawn3 } from "child_process";
|
|
11457
|
+
import fs11 from "fs";
|
|
11458
|
+
import os2 from "os";
|
|
11459
|
+
import readline3 from "readline";
|
|
11460
|
+
var logger15 = createLogger("Snapshot");
|
|
11461
|
+
function startFirecracker(nsName, firecrackerBinary, apiSocketPath, workDir) {
|
|
11462
|
+
logger15.log("Starting Firecracker with API socket...");
|
|
11463
|
+
const currentUser = os2.userInfo().username;
|
|
11464
|
+
const fcProcess = spawn3(
|
|
11465
|
+
"sudo",
|
|
11466
|
+
[
|
|
11467
|
+
"ip",
|
|
11468
|
+
"netns",
|
|
11469
|
+
"exec",
|
|
11470
|
+
nsName,
|
|
11471
|
+
"sudo",
|
|
11472
|
+
"-u",
|
|
11473
|
+
currentUser,
|
|
11474
|
+
firecrackerBinary,
|
|
11475
|
+
"--api-sock",
|
|
11476
|
+
apiSocketPath
|
|
11477
|
+
],
|
|
11478
|
+
{
|
|
11479
|
+
cwd: workDir,
|
|
11480
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11481
|
+
detached: false
|
|
11482
|
+
}
|
|
11483
|
+
);
|
|
11484
|
+
if (fcProcess.stdout) {
|
|
11485
|
+
const stdoutRL = readline3.createInterface({ input: fcProcess.stdout });
|
|
11486
|
+
stdoutRL.on("line", (line) => {
|
|
11487
|
+
if (line.trim()) logger15.log(`[FC] ${line}`);
|
|
11488
|
+
});
|
|
11489
|
+
}
|
|
11490
|
+
if (fcProcess.stderr) {
|
|
11491
|
+
const stderrRL = readline3.createInterface({ input: fcProcess.stderr });
|
|
11492
|
+
stderrRL.on("line", (line) => {
|
|
11493
|
+
if (line.trim()) logger15.log(`[FC stderr] ${line}`);
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
fcProcess.on("error", (err) => logger15.log(`Firecracker error: ${err}`));
|
|
11497
|
+
fcProcess.on(
|
|
11498
|
+
"exit",
|
|
11499
|
+
(code, signal) => logger15.log(`Firecracker exited: code=${code}, signal=${signal}`)
|
|
11500
|
+
);
|
|
11501
|
+
return fcProcess;
|
|
11502
|
+
}
|
|
11503
|
+
var snapshotCommand = new Command5("snapshot").description("Generate a Firecracker snapshot for fast VM startup").argument("<output-dir>", "Output directory for snapshot files").option("--config <path>", "Config file path", "./runner.yaml").action(
|
|
11504
|
+
async (outputDir, opts) => {
|
|
11505
|
+
const options = {
|
|
11506
|
+
config: opts.config ?? "./runner.yaml",
|
|
11507
|
+
output: outputDir
|
|
11508
|
+
};
|
|
11509
|
+
const timer = new Timer();
|
|
11510
|
+
setGlobalLogger(timer.log.bind(timer));
|
|
11511
|
+
logger15.log("Loading configuration...");
|
|
11512
|
+
const config = loadDebugConfig(options.config);
|
|
11513
|
+
validateFirecrackerPaths(config.firecracker);
|
|
11514
|
+
const nsName = "vm0-snapshot";
|
|
11515
|
+
const workDir = runnerPaths.snapshotWorkDir(config.base_dir);
|
|
11516
|
+
const overlayPath = vmPaths.overlay(workDir);
|
|
11517
|
+
const vsockPath = vmPaths.vsock(workDir);
|
|
11518
|
+
const apiSocketPath = vmPaths.apiSock(workDir);
|
|
11519
|
+
const outputSnapshot = snapshotOutputPaths.snapshot(options.output);
|
|
11520
|
+
const outputMemory = snapshotOutputPaths.memory(options.output);
|
|
11521
|
+
const outputOverlay = snapshotOutputPaths.overlay(options.output);
|
|
11522
|
+
let fcProcess = null;
|
|
11523
|
+
let vsockClient = null;
|
|
11524
|
+
let exitCode = 0;
|
|
11525
|
+
try {
|
|
11526
|
+
if (fs11.existsSync(workDir)) {
|
|
11527
|
+
logger15.log("Cleaning up stale work directory...");
|
|
11528
|
+
fs11.rmSync(workDir, { recursive: true, force: true });
|
|
11529
|
+
}
|
|
11530
|
+
logger15.log(`Creating directories...`);
|
|
11531
|
+
fs11.mkdirSync(options.output, { recursive: true });
|
|
11532
|
+
fs11.mkdirSync(workDir, { recursive: true });
|
|
11533
|
+
fs11.mkdirSync(vmPaths.vsockDir(workDir), { recursive: true });
|
|
11534
|
+
logger15.log("Creating overlay filesystem...");
|
|
11535
|
+
await createOverlayFile(overlayPath);
|
|
11536
|
+
logger15.log(`Overlay created: ${overlayPath}`);
|
|
11537
|
+
logger15.log(`Creating network namespace: ${nsName}`);
|
|
11538
|
+
await deleteNetns(nsName);
|
|
11539
|
+
await createNetnsWithTap(nsName, {
|
|
11540
|
+
tapName: SNAPSHOT_NETWORK.tapName,
|
|
11541
|
+
gatewayIpWithPrefix: `${SNAPSHOT_NETWORK.gatewayIp}/${SNAPSHOT_NETWORK.prefixLen}`
|
|
11542
|
+
});
|
|
11543
|
+
logger15.log("Network namespace created");
|
|
11544
|
+
fcProcess = startFirecracker(
|
|
11545
|
+
nsName,
|
|
11546
|
+
config.firecracker.binary,
|
|
11547
|
+
apiSocketPath,
|
|
11548
|
+
workDir
|
|
11549
|
+
);
|
|
11550
|
+
const apiClient = new FirecrackerClient(apiSocketPath);
|
|
11551
|
+
logger15.log("Waiting for API to be ready...");
|
|
11552
|
+
await apiClient.waitForReady();
|
|
11553
|
+
logger15.log("API ready");
|
|
11554
|
+
logger15.log("Configuring VM via API...");
|
|
11555
|
+
await Promise.all([
|
|
11556
|
+
apiClient.configureMachine({
|
|
11557
|
+
vcpu_count: config.sandbox.vcpu,
|
|
11558
|
+
mem_size_mib: config.sandbox.memory_mb
|
|
11559
|
+
}),
|
|
11560
|
+
apiClient.configureBootSource({
|
|
11561
|
+
kernel_image_path: config.firecracker.kernel,
|
|
11562
|
+
boot_args: buildBootArgs()
|
|
11563
|
+
}),
|
|
11564
|
+
apiClient.configureDrive({
|
|
11565
|
+
drive_id: "rootfs",
|
|
11566
|
+
path_on_host: config.firecracker.rootfs,
|
|
11567
|
+
is_root_device: true,
|
|
11568
|
+
is_read_only: true
|
|
11569
|
+
}),
|
|
11570
|
+
apiClient.configureDrive({
|
|
11571
|
+
drive_id: "overlay",
|
|
11572
|
+
path_on_host: overlayPath,
|
|
11573
|
+
is_root_device: false,
|
|
11574
|
+
is_read_only: false
|
|
11575
|
+
}),
|
|
11576
|
+
apiClient.configureNetworkInterface({
|
|
11577
|
+
iface_id: "eth0",
|
|
11578
|
+
guest_mac: SNAPSHOT_NETWORK.guestMac,
|
|
11579
|
+
host_dev_name: SNAPSHOT_NETWORK.tapName
|
|
11580
|
+
}),
|
|
11581
|
+
apiClient.configureVsock({
|
|
11582
|
+
guest_cid: 3,
|
|
11583
|
+
uds_path: vsockPath
|
|
11584
|
+
})
|
|
11585
|
+
]);
|
|
11586
|
+
logger15.log("VM configured");
|
|
11587
|
+
logger15.log("Starting vsock listener...");
|
|
11588
|
+
vsockClient = new VsockClient(vsockPath);
|
|
11589
|
+
const guestConnectionPromise = vsockClient.waitForGuestConnection(6e4);
|
|
11590
|
+
logger15.log("Starting VM...");
|
|
11591
|
+
await apiClient.startInstance();
|
|
11592
|
+
logger15.log("VM started");
|
|
11593
|
+
logger15.log("Waiting for guest connection...");
|
|
11594
|
+
await guestConnectionPromise;
|
|
11595
|
+
logger15.log("Guest connected");
|
|
11596
|
+
logger15.log("Verifying guest is responsive...");
|
|
11597
|
+
const reachable = await vsockClient.isReachable();
|
|
11598
|
+
if (!reachable) {
|
|
11599
|
+
throw new Error("Guest is not responsive");
|
|
11600
|
+
}
|
|
11601
|
+
logger15.log("Guest is responsive");
|
|
11602
|
+
logger15.log("Pausing VM...");
|
|
11603
|
+
await apiClient.pause();
|
|
11604
|
+
logger15.log("VM paused");
|
|
11605
|
+
logger15.log("Creating snapshot...");
|
|
11606
|
+
await apiClient.createSnapshot({
|
|
11607
|
+
snapshot_type: "Full",
|
|
11608
|
+
snapshot_path: outputSnapshot,
|
|
11609
|
+
mem_file_path: outputMemory
|
|
11610
|
+
});
|
|
11611
|
+
logger15.log("Snapshot created");
|
|
11612
|
+
logger15.log("Copying overlay as golden overlay...");
|
|
11613
|
+
await execCommand(
|
|
11614
|
+
`cp --sparse=always "${overlayPath}" "${outputOverlay}"`,
|
|
11615
|
+
false
|
|
11616
|
+
);
|
|
11617
|
+
logger15.log("Golden overlay created");
|
|
11618
|
+
logger15.log("=".repeat(40));
|
|
11619
|
+
logger15.log("Snapshot generation complete!");
|
|
11620
|
+
logger15.log("Files (logical size):");
|
|
11621
|
+
const lsOutput = await execCommand(`ls -lh "${options.output}"`, false);
|
|
11622
|
+
logger15.log(lsOutput);
|
|
11623
|
+
logger15.log("Actual disk usage:");
|
|
11624
|
+
const duOutput = await execCommand(
|
|
11625
|
+
`du -h "${options.output}"/*`,
|
|
11626
|
+
false
|
|
11627
|
+
);
|
|
11628
|
+
logger15.log(duOutput);
|
|
11629
|
+
logger15.log("=".repeat(40));
|
|
11630
|
+
} catch (error) {
|
|
11631
|
+
logger15.error(
|
|
11632
|
+
`Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
11633
|
+
);
|
|
11634
|
+
exitCode = 1;
|
|
11635
|
+
} finally {
|
|
11636
|
+
logger15.log("Cleaning up...");
|
|
11637
|
+
if (vsockClient) {
|
|
11638
|
+
vsockClient.close();
|
|
11639
|
+
}
|
|
11640
|
+
if (fcProcess && !fcProcess.killed) {
|
|
11641
|
+
fcProcess.kill("SIGKILL");
|
|
11642
|
+
}
|
|
11643
|
+
await execCommand(
|
|
11644
|
+
`pkill -9 -f "firecracker.*${apiSocketPath}"`,
|
|
11645
|
+
true
|
|
11646
|
+
).catch(() => {
|
|
11647
|
+
});
|
|
11648
|
+
await deleteNetns(nsName);
|
|
11649
|
+
if (fs11.existsSync(workDir)) {
|
|
11650
|
+
fs11.rmSync(workDir, { recursive: true, force: true });
|
|
11651
|
+
}
|
|
11652
|
+
logger15.log("Cleanup complete");
|
|
11653
|
+
}
|
|
11654
|
+
if (exitCode !== 0) {
|
|
11655
|
+
process.exit(exitCode);
|
|
11656
|
+
}
|
|
11657
|
+
}
|
|
11658
|
+
);
|
|
11659
|
+
|
|
11357
11660
|
// src/index.ts
|
|
11358
|
-
var version = true ? "3.
|
|
11661
|
+
var version = true ? "3.12.0" : "0.1.0";
|
|
11359
11662
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
11360
11663
|
program.addCommand(startCommand);
|
|
11361
11664
|
program.addCommand(doctorCommand);
|
|
11362
11665
|
program.addCommand(killCommand);
|
|
11363
11666
|
program.addCommand(benchmarkCommand);
|
|
11667
|
+
program.addCommand(snapshotCommand);
|
|
11364
11668
|
program.parse();
|
|
11365
11669
|
//# sourceMappingURL=index.js.map
|