@vm0/runner 3.9.4 → 3.10.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.
Files changed (2) hide show
  1. package/index.js +61 -33
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1352,12 +1352,15 @@ var TapPool = class {
1352
1352
  }
1353
1353
  const guestMac = generateMacAddress(vmId);
1354
1354
  try {
1355
- await this.config.setMac(resource.tapDevice, guestMac);
1355
+ await Promise.all([
1356
+ this.config.setMac(resource.tapDevice, guestMac),
1357
+ clearArpEntry(resource.guestIp)
1358
+ ]);
1356
1359
  } catch (err) {
1357
1360
  if (fromPool) {
1358
1361
  this.queue.push(resource);
1359
1362
  logger4.log(
1360
- `Returned pair to pool after MAC set failure: ${resource.tapDevice}`
1363
+ `Returned pair to pool after MAC/ARP failure: ${resource.tapDevice}`
1361
1364
  );
1362
1365
  } else {
1363
1366
  await releaseIP(resource.guestIp).catch(() => {
@@ -1367,14 +1370,11 @@ var TapPool = class {
1367
1370
  }
1368
1371
  throw err;
1369
1372
  }
1370
- await clearArpEntry(resource.guestIp);
1371
- try {
1372
- await assignVmIdToIP(resource.guestIp, vmId);
1373
- } catch (err) {
1373
+ assignVmIdToIP(resource.guestIp, vmId).catch((err) => {
1374
1374
  logger4.error(
1375
1375
  `Failed to assign vmId to IP registry: ${err instanceof Error ? err.message : "Unknown"}`
1376
1376
  );
1377
- }
1377
+ });
1378
1378
  logger4.log(
1379
1379
  `Acquired: TAP ${resource.tapDevice}, MAC ${guestMac}, IP ${resource.guestIp}`
1380
1380
  );
@@ -1695,12 +1695,9 @@ var FirecrackerVM = class {
1695
1695
  /**
1696
1696
  * Stop the VM
1697
1697
  *
1698
- * Note: With --no-api mode, we can only force kill the process.
1699
- * The VM doesn't have an API endpoint for graceful shutdown.
1700
- *
1701
- * TODO(#2118): Implement graceful shutdown via vsock command to guest agent.
1702
- * This would allow the guest to clean up before termination without
1703
- * adding the startup latency of API mode.
1698
+ * Note: With --no-api mode, we force kill the Firecracker process.
1699
+ * Graceful shutdown (filesystem sync) should be done via vsock
1700
+ * before calling this method.
1704
1701
  */
1705
1702
  async stop() {
1706
1703
  if (this.state !== "running") {
@@ -1794,6 +1791,8 @@ var MSG_WRITE_FILE = 5;
1794
1791
  var MSG_SPAWN_WATCH = 7;
1795
1792
  var MSG_SPAWN_WATCH_RESULT = 8;
1796
1793
  var MSG_PROCESS_EXIT = 9;
1794
+ var MSG_SHUTDOWN = 10;
1795
+ var MSG_SHUTDOWN_ACK = 11;
1797
1796
  var MSG_ERROR = 255;
1798
1797
  var FLAG_SUDO = 1;
1799
1798
  function encode(type, seq, payload = Buffer.alloc(0)) {
@@ -2295,6 +2294,30 @@ var VsockClient = class {
2295
2294
  getVsockPath() {
2296
2295
  return this.vsockPath;
2297
2296
  }
2297
+ /**
2298
+ * Request graceful shutdown from guest
2299
+ *
2300
+ * Sends shutdown command to guest agent, which syncs filesystems
2301
+ * and returns acknowledgment. Falls back gracefully on timeout.
2302
+ *
2303
+ * @param timeoutMs Maximum time to wait for acknowledgment (default: 2000ms)
2304
+ * @returns true if guest acknowledged, false on timeout/error
2305
+ */
2306
+ async shutdown(timeoutMs = 2e3) {
2307
+ if (!this.connected || !this.socket) {
2308
+ return false;
2309
+ }
2310
+ try {
2311
+ const response = await this.request(
2312
+ MSG_SHUTDOWN,
2313
+ Buffer.alloc(0),
2314
+ timeoutMs
2315
+ );
2316
+ return response.type === MSG_SHUTDOWN_ACK;
2317
+ } catch {
2318
+ return false;
2319
+ }
2320
+ }
2298
2321
  /**
2299
2322
  * Close the connection
2300
2323
  */
@@ -10033,6 +10056,7 @@ async function executeJob(context, config, options = {}) {
10033
10056
  const vmId = getVmIdFromRunId(context.runId);
10034
10057
  let vm = null;
10035
10058
  let guestIp = null;
10059
+ let vsockClient = null;
10036
10060
  logger10.log(`Starting job ${context.runId} in VM ${vmId}`);
10037
10061
  try {
10038
10062
  const vmConfig = {
@@ -10053,14 +10077,12 @@ async function executeJob(context, config, options = {}) {
10053
10077
  }
10054
10078
  logger10.log(`VM ${vmId} started, guest IP: ${guestIp}`);
10055
10079
  const vsockPath = vm.getVsockPath();
10056
- const guest = new VsockClient(vsockPath);
10080
+ vsockClient = new VsockClient(vsockPath);
10081
+ const guest = vsockClient;
10057
10082
  logger10.log(`Using vsock for guest communication: ${vsockPath}`);
10058
- logger10.log(`Waiting for guest connection...`);
10059
- await withSandboxTiming(
10060
- "guest_wait",
10061
- () => guest.waitForGuestConnection(3e4)
10083
+ const envJson = JSON.stringify(
10084
+ buildEnvironmentVariables(context, config.server.url)
10062
10085
  );
10063
- logger10.log(`Guest client ready`);
10064
10086
  const firewallConfig = context.experimentalFirewall;
10065
10087
  if (firewallConfig?.enabled) {
10066
10088
  const mitmEnabled = firewallConfig.experimental_mitm ?? false;
@@ -10068,19 +10090,18 @@ async function executeJob(context, config, options = {}) {
10068
10090
  logger10.log(
10069
10091
  `Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
10070
10092
  );
10071
- await withSandboxTiming("network_setup", async () => {
10072
- getVMRegistry().register(
10073
- guestIp,
10074
- context.runId,
10075
- context.sandboxToken,
10076
- {
10077
- firewallRules: firewallConfig?.rules,
10078
- mitmEnabled,
10079
- sealSecretsEnabled
10080
- }
10081
- );
10093
+ getVMRegistry().register(guestIp, context.runId, context.sandboxToken, {
10094
+ firewallRules: firewallConfig?.rules,
10095
+ mitmEnabled,
10096
+ sealSecretsEnabled
10082
10097
  });
10083
10098
  }
10099
+ logger10.log(`Waiting for guest connection...`);
10100
+ await withSandboxTiming(
10101
+ "guest_wait",
10102
+ () => guest.waitForGuestConnection(3e4)
10103
+ );
10104
+ logger10.log(`Guest client ready`);
10084
10105
  if (context.storageManifest) {
10085
10106
  await withSandboxTiming(
10086
10107
  "storage_download",
@@ -10098,8 +10119,6 @@ async function executeJob(context, config, options = {}) {
10098
10119
  )
10099
10120
  );
10100
10121
  }
10101
- const envVars = buildEnvironmentVariables(context, config.server.url);
10102
- const envJson = JSON.stringify(envVars);
10103
10122
  logger10.log(
10104
10123
  `Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
10105
10124
  );
@@ -10196,6 +10215,15 @@ async function executeJob(context, config, options = {}) {
10196
10215
  }
10197
10216
  }
10198
10217
  if (vm) {
10218
+ if (vsockClient) {
10219
+ const acked = await vsockClient.shutdown(2e3);
10220
+ if (acked) {
10221
+ logger10.log(`Guest acknowledged shutdown`);
10222
+ } else {
10223
+ logger10.log(`Guest shutdown timeout, proceeding with SIGKILL`);
10224
+ }
10225
+ vsockClient.close();
10226
+ }
10199
10227
  logger10.log(`Cleaning up VM ${vmId}...`);
10200
10228
  await withSandboxTiming("cleanup", () => vm.kill());
10201
10229
  }
@@ -11232,7 +11260,7 @@ var benchmarkCommand = new Command4("benchmark").description(
11232
11260
  });
11233
11261
 
11234
11262
  // src/index.ts
11235
- var version = true ? "3.9.4" : "0.1.0";
11263
+ var version = true ? "3.10.0" : "0.1.0";
11236
11264
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
11237
11265
  program.addCommand(startCommand);
11238
11266
  program.addCommand(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.9.4",
3
+ "version": "3.10.0",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",