@machinen/runtime 0.1.2 → 0.3.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/dist/index.js CHANGED
@@ -1,23 +1,139 @@
1
- import {
2
- BootError,
3
- CacheError,
4
- ErrorCode,
5
- ExecError,
6
- FilesError,
7
- GvproxyError,
8
- MachinenError,
9
- MkinitramfsError,
10
- MountError,
11
- ParseError,
12
- ProvisionError,
13
- RegistryError,
14
- SandboxError,
15
- SecretsError,
16
- SnapshotError,
17
- WinsizeError,
18
- formatMachinenError,
19
- isMachinenError
20
- } from "./chunk-6SP6T537.js";
1
+ // src/errors.ts
2
+ var ErrorCode = {
3
+ // boot — VM lifecycle, binary lookup, config validation
4
+ BOOT_VMM_MISSING: "BOOT_VMM_MISSING",
5
+ BOOT_VMM_PACKAGE_BROKEN: "BOOT_VMM_PACKAGE_BROKEN",
6
+ BOOT_IMAGE_NOT_FOUND: "BOOT_IMAGE_NOT_FOUND",
7
+ BOOT_SNAPSHOT_NOT_FOUND: "BOOT_SNAPSHOT_NOT_FOUND",
8
+ BOOT_KERNEL_NOT_FOUND: "BOOT_KERNEL_NOT_FOUND",
9
+ BOOT_DTB_NOT_FOUND: "BOOT_DTB_NOT_FOUND",
10
+ BOOT_CMD_WITHOUT_IMAGE: "BOOT_CMD_WITHOUT_IMAGE",
11
+ BOOT_CMD_MISSING: "BOOT_CMD_MISSING",
12
+ BOOT_CWD_INVALID: "BOOT_CWD_INVALID",
13
+ BOOT_MOUNT_INVALID: "BOOT_MOUNT_INVALID",
14
+ BOOT_MOUNT_HOST_NOT_FOUND: "BOOT_MOUNT_HOST_NOT_FOUND",
15
+ BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN: "BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN",
16
+ BOOT_PORT_FORWARD_INVALID: "BOOT_PORT_FORWARD_INVALID",
17
+ BOOT_PORT_FORWARD_CONFLICT: "BOOT_PORT_FORWARD_CONFLICT",
18
+ BOOT_PORT_FORWARD_NO_GVPROXY: "BOOT_PORT_FORWARD_NO_GVPROXY",
19
+ BOOT_PORT_FORWARD_IN_USE: "BOOT_PORT_FORWARD_IN_USE",
20
+ BOOT_PACK_FAILED: "BOOT_PACK_FAILED",
21
+ BOOT_TIMEOUT: "BOOT_TIMEOUT",
22
+ BOOT_DETACHED_READINESS_FAILED: "BOOT_DETACHED_READINESS_FAILED",
23
+ BOOT_MEMORY_INVALID: "BOOT_MEMORY_INVALID",
24
+ FORK_MEMORY_BACKPRESSURE: "FORK_MEMORY_BACKPRESSURE",
25
+ // mountdisk — `--mount` squashfs+ext4 overlay (#272)
26
+ BOOT_MOUNTDISK_TOOL_MISSING: "BOOT_MOUNTDISK_TOOL_MISSING",
27
+ // exec — vsock command execution
28
+ EXEC_VSOCK_UNAVAILABLE: "EXEC_VSOCK_UNAVAILABLE",
29
+ EXEC_AGENT_UNAVAILABLE: "EXEC_AGENT_UNAVAILABLE",
30
+ EXEC_AGENT_TIMEOUT: "EXEC_AGENT_TIMEOUT",
31
+ EXEC_NONZERO_EXIT: "EXEC_NONZERO_EXIT",
32
+ EXEC_PROTOCOL: "EXEC_PROTOCOL",
33
+ // snapshot — CRIU-based VM snapshotting
34
+ SNAPSHOT_NO_DISK: "SNAPSHOT_NO_DISK",
35
+ SNAPSHOT_DUMP_FAILED: "SNAPSHOT_DUMP_FAILED",
36
+ SNAPSHOT_TIMEOUT: "SNAPSHOT_TIMEOUT",
37
+ // provision — image builder
38
+ PROVISION_BASE_NOT_FOUND: "PROVISION_BASE_NOT_FOUND",
39
+ PROVISION_KERNEL_NOT_FOUND: "PROVISION_KERNEL_NOT_FOUND",
40
+ PROVISION_DTB_NOT_FOUND: "PROVISION_DTB_NOT_FOUND",
41
+ PROVISION_ASSETS_DIR_INVALID: "PROVISION_ASSETS_DIR_INVALID",
42
+ PROVISION_INSTALL_HOOK_FAILED: "PROVISION_INSTALL_HOOK_FAILED",
43
+ PROVISION_DISK_TOO_SMALL: "PROVISION_DISK_TOO_SMALL",
44
+ // rootfs-img — tar→ext4 materializer (#114, #120)
45
+ ROOTFS_IMG_TOOL_MISSING: "ROOTFS_IMG_TOOL_MISSING",
46
+ // registry / attach
47
+ REGISTRY_VM_NOT_FOUND: "REGISTRY_VM_NOT_FOUND",
48
+ REGISTRY_NAME_IN_USE: "REGISTRY_NAME_IN_USE",
49
+ // files — vsock push/pull
50
+ FILES_HOST_DIR_NOT_FOUND: "FILES_HOST_DIR_NOT_FOUND",
51
+ FILES_AGENT_UNAVAILABLE: "FILES_AGENT_UNAVAILABLE",
52
+ // mount — live-share (--mount-live, #78). Path containment is the
53
+ // load-bearing piece: any resolver failure means the guest tried to
54
+ // reach outside the mounted directory.
55
+ MOUNT_PATH_INVALID: "MOUNT_PATH_INVALID",
56
+ MOUNT_PATH_ESCAPE: "MOUNT_PATH_ESCAPE",
57
+ // secrets — vsock KEY=VALUE injection
58
+ SECRETS_VALUE_INVALID: "SECRETS_VALUE_INVALID",
59
+ SECRETS_AGENT_UNAVAILABLE: "SECRETS_AGENT_UNAVAILABLE",
60
+ // winsize — vsock resize forwarder
61
+ WINSIZE_AGENT_UNAVAILABLE: "WINSIZE_AGENT_UNAVAILABLE",
62
+ // multiplex — sandbox registry
63
+ SANDBOX_ID_DUPLICATE: "SANDBOX_ID_DUPLICATE",
64
+ SANDBOX_ID_UNKNOWN: "SANDBOX_ID_UNKNOWN",
65
+ // cache — host-side artifact mirror
66
+ CACHE_BIND_FAILED: "CACHE_BIND_FAILED",
67
+ // gvproxy — user-mode network bridge
68
+ GVPROXY_NOT_FOUND: "GVPROXY_NOT_FOUND",
69
+ GVPROXY_EXPOSE_FAILED: "GVPROXY_EXPOSE_FAILED",
70
+ GVPROXY_PORT_IN_USE: "GVPROXY_PORT_IN_USE",
71
+ GVPROXY_INSTALL_FAILED: "GVPROXY_INSTALL_FAILED",
72
+ GVPROXY_SPAWN_FAILED: "GVPROXY_SPAWN_FAILED",
73
+ // mkinitramfs — initramfs packer
74
+ MKINITRAMFS_BUNDLE_INVALID: "MKINITRAMFS_BUNDLE_INVALID",
75
+ MKINITRAMFS_WORKSPACE_INVALID: "MKINITRAMFS_WORKSPACE_INVALID",
76
+ MKINITRAMFS_WORKSPACE_TOO_LARGE: "MKINITRAMFS_WORKSPACE_TOO_LARGE",
77
+ MKINITRAMFS_BASE_EXTRACT_FAILED: "MKINITRAMFS_BASE_EXTRACT_FAILED",
78
+ MKINITRAMFS_INIT_MISSING: "MKINITRAMFS_INIT_MISSING",
79
+ // parse — CLI arg parser
80
+ PARSE_FLAG_UNKNOWN: "PARSE_FLAG_UNKNOWN",
81
+ PARSE_FLAG_MISSING_VALUE: "PARSE_FLAG_MISSING_VALUE",
82
+ PARSE_FLAG_DUPLICATE: "PARSE_FLAG_DUPLICATE",
83
+ PARSE_FLAG_MALFORMED: "PARSE_FLAG_MALFORMED",
84
+ PARSE_PORT_INVALID: "PARSE_PORT_INVALID"
85
+ };
86
+ var MachinenError = class extends Error {
87
+ code;
88
+ retryable;
89
+ constructor(code, message, opts = {}) {
90
+ super(message, { cause: opts.cause });
91
+ this.code = code;
92
+ this.retryable = opts.retryable ?? false;
93
+ this.name = new.target.name;
94
+ }
95
+ };
96
+ var BootError = class extends MachinenError {
97
+ };
98
+ var ExecError = class extends MachinenError {
99
+ };
100
+ var SnapshotError = class extends MachinenError {
101
+ };
102
+ var ProvisionError = class extends MachinenError {
103
+ };
104
+ var RegistryError = class extends MachinenError {
105
+ };
106
+ var FilesError = class extends MachinenError {
107
+ };
108
+ var MountError = class extends MachinenError {
109
+ };
110
+ var SecretsError = class extends MachinenError {
111
+ };
112
+ var WinsizeError = class extends MachinenError {
113
+ };
114
+ var SandboxError = class extends MachinenError {
115
+ };
116
+ var CacheError = class extends MachinenError {
117
+ };
118
+ var GvproxyError = class extends MachinenError {
119
+ };
120
+ var MkinitramfsError = class extends MachinenError {
121
+ };
122
+ var ParseError = class extends MachinenError {
123
+ };
124
+ function isMachinenError(err, code) {
125
+ return err instanceof MachinenError && (code === void 0 || err.code === code);
126
+ }
127
+ function formatMachinenError(err) {
128
+ const lines = [`(${err.code}): ${err.message}`];
129
+ let cur = err.cause;
130
+ while (cur !== void 0 && cur !== null) {
131
+ const msg = cur instanceof Error ? cur.message : String(cur);
132
+ lines.push(` caused by: ${msg}`);
133
+ cur = cur instanceof Error ? cur.cause : void 0;
134
+ }
135
+ return lines.join("\n");
136
+ }
21
137
 
22
138
  // src/multiplex.ts
23
139
  var Sandboxes = class {
@@ -877,11 +993,17 @@ async function runOnSocket(socket, cmd, opts) {
877
993
  buf = buf.subarray(take);
878
994
  awaitingBytes -= take;
879
995
  if (payloadTag === "O") {
880
- stdoutBufs.push(chunk);
881
- opts.onStdout?.(chunk);
996
+ if (opts.onStdout) {
997
+ opts.onStdout(chunk);
998
+ } else {
999
+ stdoutBufs.push(chunk);
1000
+ }
882
1001
  } else if (payloadTag === "E") {
883
- stderrBufs.push(chunk);
884
- opts.onStderr?.(chunk);
1002
+ if (opts.onStderr) {
1003
+ opts.onStderr(chunk);
1004
+ } else {
1005
+ stderrBufs.push(chunk);
1006
+ }
885
1007
  }
886
1008
  if (awaitingBytes === 0) {
887
1009
  payloadTag = null;
@@ -1152,19 +1274,19 @@ function connectOnceWithTimeout(udsPath, timeoutMs) {
1152
1274
  import { execFileSync as execFileSync9 } from "child_process";
1153
1275
  import {
1154
1276
  closeSync as closeSync7,
1155
- existsSync as existsSync17,
1277
+ existsSync as existsSync16,
1156
1278
  mkdirSync as mkdirSync12,
1157
1279
  mkdtempSync as mkdtempSync8,
1158
1280
  openSync as openSync7,
1159
- readFileSync as readFileSync11,
1160
- rmSync as rmSync11,
1281
+ readFileSync as readFileSync10,
1282
+ rmSync as rmSync10,
1161
1283
  statSync as statSync11,
1162
1284
  writeFileSync as writeFileSync9,
1163
1285
  writeSync as writeSync6
1164
1286
  } from "fs";
1165
1287
  import { homedir as homedir8, tmpdir as tmpdir8 } from "os";
1166
- import { dirname as dirname9, join as join16, resolve as resolve9 } from "path";
1167
- import debugLib16 from "debug";
1288
+ import { dirname as dirname7, join as join16, resolve as resolve9 } from "path";
1289
+ import debugLib15 from "debug";
1168
1290
 
1169
1291
  // src/phase-timer.ts
1170
1292
  var PhaseTimer = class {
@@ -1265,7 +1387,7 @@ function reflinkCopy(src, dst) {
1265
1387
  }
1266
1388
 
1267
1389
  // src/vm/attach.ts
1268
- import debugLib15 from "debug";
1390
+ import debugLib14 from "debug";
1269
1391
 
1270
1392
  // src/balloon-stats.ts
1271
1393
  import { readFileSync } from "fs";
@@ -1539,15 +1661,6 @@ function writeEntry(entry) {
1539
1661
  }
1540
1662
  debug2("write pid=%d name=%s sock=%s", entry.pid, entry.name ?? "<unset>", entry.socketPath);
1541
1663
  }
1542
- function patchEntry(pid, patch) {
1543
- const existing = readEntry(pid);
1544
- if (!existing) {
1545
- debug2("patch pid=%d skipped (no entry)", pid);
1546
- return;
1547
- }
1548
- const merged = { ...existing, ...patch, pid: existing.pid };
1549
- writeEntry(merged);
1550
- }
1551
1664
  function removeEntry(pid) {
1552
1665
  debug2("remove pid=%d", pid);
1553
1666
  const root = registryRoot();
@@ -1816,10 +1929,10 @@ function findEntry(query) {
1816
1929
  }
1817
1930
 
1818
1931
  // src/vm/fork.ts
1819
- import { mkdtempSync as mkdtempSync7, rmSync as rmSync10 } from "fs";
1932
+ import { mkdtempSync as mkdtempSync7, rmSync as rmSync9 } from "fs";
1820
1933
  import { tmpdir as tmpdir7 } from "os";
1821
1934
  import { join as join15, resolve as resolve8 } from "path";
1822
- import debugLib14 from "debug";
1935
+ import debugLib13 from "debug";
1823
1936
 
1824
1937
  // src/host-mem.ts
1825
1938
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1890,17 +2003,17 @@ import { execFileSync as execFileSync8 } from "child_process";
1890
2003
  import { randomBytes as randomBytes4 } from "crypto";
1891
2004
  import {
1892
2005
  closeSync as closeSync6,
1893
- existsSync as existsSync16,
2006
+ existsSync as existsSync15,
1894
2007
  openSync as openSync6,
1895
2008
  readdirSync as readdirSync7,
1896
- readFileSync as readFileSync10,
2009
+ readFileSync as readFileSync9,
1897
2010
  statSync as statSync10,
1898
- unlinkSync as unlinkSync8,
2011
+ unlinkSync as unlinkSync9,
1899
2012
  writeSync as writeSync5
1900
2013
  } from "fs";
1901
2014
  import { tmpdir as tmpdir6 } from "os";
1902
2015
  import { join as join14, resolve as resolve7 } from "path";
1903
- import debugLib13 from "debug";
2016
+ import debugLib12 from "debug";
1904
2017
 
1905
2018
  // src/lazy-pagemap.ts
1906
2019
  import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
@@ -2180,21 +2293,21 @@ function skipField(buf, pos, wireType) {
2180
2293
  }
2181
2294
 
2182
2295
  // src/vm/boot.ts
2183
- import { spawn as nodeSpawn4 } from "child_process";
2296
+ import { spawn as nodeSpawn3 } from "child_process";
2184
2297
  import { randomBytes as randomBytes3 } from "crypto";
2185
2298
  import { once } from "events";
2186
2299
  import {
2187
2300
  closeSync as closeSync5,
2188
- existsSync as existsSync15,
2301
+ existsSync as existsSync14,
2189
2302
  mkdtempSync as mkdtempSync6,
2190
2303
  openSync as openSync5,
2191
- rmSync as rmSync9,
2192
- unlinkSync as unlinkSync7,
2304
+ rmSync as rmSync8,
2305
+ unlinkSync as unlinkSync8,
2193
2306
  writeSync as writeSync4
2194
2307
  } from "fs";
2195
2308
  import { tmpdir as tmpdir5 } from "os";
2196
2309
  import { join as join13, resolve as resolve6 } from "path";
2197
- import debugLib12 from "debug";
2310
+ import debugLib11 from "debug";
2198
2311
 
2199
2312
  // src/detached-log.ts
2200
2313
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
@@ -2324,7 +2437,7 @@ import { homedir as homedir3, platform as osPlatform3 } from "os";
2324
2437
  import { dirname as dirname4, join as join5 } from "path";
2325
2438
  import debugLib4 from "debug";
2326
2439
  var debug4 = debugLib4("machinen:pdeathsig");
2327
- var PDEATHSIG_VERSION = "v3";
2440
+ var PDEATHSIG_VERSION = "v4";
2328
2441
  var warnedNoCompiler = false;
2329
2442
  var installInFlight = /* @__PURE__ */ new Map();
2330
2443
  var PDEATHSIG_C_SOURCE = `// pdeathsig: tiny exec wrapper that arranges for the target to die
@@ -2519,11 +2632,19 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2519
2632
  // the default disposition would kill the guard before kqueue
2520
2633
  // delivers the event, leaving the target alive and orphaned to
2521
2634
  // PID 1 \u2014 defeating the whole point of the shim.
2635
+ //
2636
+ // SIGUSR1 is included for the vmstate snapshot trigger: the
2637
+ // runtime signals the wrapped VMM's pid, which on macOS is *this*
2638
+ // shim (the watch loop forks). Blocking + kqueue-ing it lets us
2639
+ // forward it to the VMM instead of dying by the default
2640
+ // disposition. The child unblocks the whole mask before execvp,
2641
+ // so the VMM's own SIGUSR1 handler still fires.
2522
2642
  sigset_t mask;
2523
2643
  sigemptyset(&mask);
2524
2644
  sigaddset(&mask, SIGTERM);
2525
2645
  sigaddset(&mask, SIGINT);
2526
2646
  sigaddset(&mask, SIGHUP);
2647
+ sigaddset(&mask, SIGUSR1);
2527
2648
  sigprocmask(SIG_BLOCK, &mask, NULL);
2528
2649
 
2529
2650
  pid_t child = fork();
@@ -2548,7 +2669,7 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2548
2669
  return reap_then_exit(child, 127);
2549
2670
  }
2550
2671
 
2551
- struct kevent changes[5];
2672
+ struct kevent changes[6];
2552
2673
  EV_SET(&changes[0], watch_pid, EVFILT_PROC, EV_ADD | EV_ONESHOT,
2553
2674
  NOTE_EXIT, 0, NULL);
2554
2675
  EV_SET(&changes[1], child, EVFILT_PROC, EV_ADD | EV_ONESHOT,
@@ -2556,7 +2677,8 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2556
2677
  EV_SET(&changes[2], SIGTERM, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2557
2678
  EV_SET(&changes[3], SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2558
2679
  EV_SET(&changes[4], SIGHUP, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2559
- if (kevent(kq, changes, 5, NULL, 0, NULL) == -1) {
2680
+ EV_SET(&changes[5], SIGUSR1, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2681
+ if (kevent(kq, changes, 6, NULL, 0, NULL) == -1) {
2560
2682
  perror("pdeathsig: kevent register");
2561
2683
  kill(child, SIGTERM);
2562
2684
  return reap_then_exit(child, 127);
@@ -2581,6 +2703,13 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2581
2703
  }
2582
2704
  if (n == 0) continue;
2583
2705
  if (ev.filter == EVFILT_SIGNAL) {
2706
+ if ((int)ev.ident == SIGUSR1) {
2707
+ // Vmstate snapshot trigger. Forward to the VMM and
2708
+ // keep watching \u2014 the VMM dumps its whole-VM state
2709
+ // and resumes the guest; it is NOT a shutdown signal.
2710
+ kill(child, SIGUSR1);
2711
+ continue;
2712
+ }
2584
2713
  // Runtime is signaling us (typical: child.kill('SIGTERM')
2585
2714
  // from the Node side, or Ctrl-C). Forward to the target
2586
2715
  // and reap. Keep the original signal so callers that look
@@ -3155,201 +3284,19 @@ function warnGvproxyMissing() {
3155
3284
  );
3156
3285
  }
3157
3286
 
3158
- // src/mount-server-detached.ts
3159
- import { spawn as nodeSpawn2 } from "child_process";
3160
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
3161
- import { fileURLToPath } from "url";
3162
- import debugLib6 from "debug";
3163
- var debug6 = debugLib6("machinen:mount-server-detached");
3164
- async function spawnDetachedMountServer(opts) {
3165
- if (!Number.isInteger(opts.vmmPid) || opts.vmmPid <= 0) {
3166
- throw new MountError(
3167
- "MOUNT_SERVER_SPAWN_FAILED",
3168
- `spawnDetachedMountServer: invalid vmmPid ${opts.vmmPid}`
3169
- );
3170
- }
3171
- const resolved = resolveMountServerCommand();
3172
- const helperArgs = [
3173
- ...resolved.args,
3174
- "--uds",
3175
- opts.udsPath,
3176
- "--root",
3177
- opts.rootAbs,
3178
- "--mode",
3179
- opts.mode,
3180
- "--stats",
3181
- opts.statsPath
3182
- ];
3183
- const pdeathsigBin = await ensurePdeathsig();
3184
- const wrapped = wrapWithPdeathsig(pdeathsigBin, resolved.command, helperArgs, {
3185
- watchPid: opts.vmmPid
3186
- });
3187
- debug6(
3188
- "spawn uds=%s root=%s mode=%s vmm=%d shim=%s",
3189
- opts.udsPath,
3190
- opts.rootAbs,
3191
- opts.mode,
3192
- opts.vmmPid,
3193
- pdeathsigBin ? "yes" : "no"
3194
- );
3195
- const child = nodeSpawn2(wrapped.command, wrapped.args, {
3196
- stdio: ["ignore", "pipe", "pipe"]
3197
- });
3198
- child.unref();
3199
- child.stderr?.on("data", (chunk) => {
3200
- debug6("stderr: %s", chunk.toString().trimEnd());
3201
- });
3202
- child.stdout?.on("data", () => {
3203
- });
3204
- await waitForSocketOrExit(child, opts.udsPath);
3205
- const pid = child.pid;
3206
- if (pid === void 0 || pid <= 0) {
3207
- throw new MountError(
3208
- "MOUNT_SERVER_SPAWN_FAILED",
3209
- "spawnDetachedMountServer: helper started but pid is unset"
3210
- );
3211
- }
3212
- return makeHandle(child, pid, wrapped.command, opts);
3213
- }
3214
- function makeHandle(child, pid, exe, opts) {
3215
- let stopped = false;
3216
- return {
3217
- pid,
3218
- exe,
3219
- bytesServedOnPagesImg: () => readBytesServed(opts.statsPath),
3220
- stop: async () => {
3221
- if (stopped) {
3222
- return;
3223
- }
3224
- stopped = true;
3225
- if (child.exitCode === null && child.signalCode === null) {
3226
- try {
3227
- child.kill("SIGTERM");
3228
- } catch {
3229
- }
3230
- }
3231
- await waitForExit(child, 5e3);
3232
- for (const path of [opts.statsPath, opts.udsPath]) {
3233
- try {
3234
- rmSync4(path, { force: true });
3235
- } catch {
3236
- }
3237
- }
3238
- }
3239
- };
3240
- }
3241
- function readBytesServed(statsPath) {
3242
- try {
3243
- const raw = readFileSync7(statsPath, "utf8");
3244
- const parsed = JSON.parse(raw);
3245
- if (parsed && typeof parsed === "object" && "bytesServedOnPagesImg" in parsed && typeof parsed.bytesServedOnPagesImg === "number") {
3246
- return parsed.bytesServedOnPagesImg;
3247
- }
3248
- return 0;
3249
- } catch {
3250
- return 0;
3251
- }
3252
- }
3253
- async function waitForExit(child, timeoutMs) {
3254
- if (child.exitCode !== null || child.signalCode !== null) {
3255
- return;
3256
- }
3257
- await new Promise((resolve10) => {
3258
- const timer = setTimeout(() => {
3259
- try {
3260
- child.kill("SIGKILL");
3261
- } catch {
3262
- }
3263
- resolve10();
3264
- }, timeoutMs);
3265
- timer.unref();
3266
- child.once("exit", () => {
3267
- clearTimeout(timer);
3268
- resolve10();
3269
- });
3270
- });
3271
- }
3272
- async function waitForSocketOrExit(child, udsPath) {
3273
- const deadline = Date.now() + 3e3;
3274
- let earlyExitErr = null;
3275
- const onExit = (code, signal) => {
3276
- earlyExitErr = new Error(
3277
- `mount-server helper exited before socket was ready (code=${code} signal=${signal})`
3278
- );
3279
- };
3280
- child.once("exit", onExit);
3281
- try {
3282
- while (Date.now() < deadline) {
3283
- if (existsSync7(udsPath)) {
3284
- return;
3285
- }
3286
- if (earlyExitErr) {
3287
- throw earlyExitErr;
3288
- }
3289
- await new Promise((r) => setTimeout(r, 25));
3290
- }
3291
- if (earlyExitErr) {
3292
- throw earlyExitErr;
3293
- }
3294
- try {
3295
- child.kill("SIGKILL");
3296
- } catch {
3297
- }
3298
- throw new MountError(
3299
- "MOUNT_SERVER_SPAWN_FAILED",
3300
- `mount-server helper did not bind ${udsPath} within 3000ms`
3301
- );
3302
- } finally {
3303
- child.removeListener("exit", onExit);
3304
- }
3305
- }
3306
- var cachedCommand = null;
3307
- function resolveMountServerCommand() {
3308
- if (cachedCommand) {
3309
- return cachedCommand;
3310
- }
3311
- const override = process.env.MACHINEN_MOUNT_SERVER_BIN;
3312
- if (override) {
3313
- if (!existsSync7(override)) {
3314
- throw new MountError(
3315
- "MOUNT_SERVER_BIN_MISSING",
3316
- `MACHINEN_MOUNT_SERVER_BIN=${override} does not exist`
3317
- );
3318
- }
3319
- cachedCommand = override.endsWith(".ts") ? { command: process.execPath, args: ["--import", "tsx", override] } : { command: process.execPath, args: [override] };
3320
- return cachedCommand;
3321
- }
3322
- const distUrl = new URL("./mount-server-bin.js", import.meta.url);
3323
- const distPath = fileURLToPath(distUrl);
3324
- if (existsSync7(distPath)) {
3325
- cachedCommand = { command: process.execPath, args: [distPath] };
3326
- return cachedCommand;
3327
- }
3328
- const srcUrl = new URL("./mount-server-bin.ts", import.meta.url);
3329
- const srcPath = fileURLToPath(srcUrl);
3330
- if (existsSync7(srcPath)) {
3331
- cachedCommand = { command: process.execPath, args: ["--import", "tsx", srcPath] };
3332
- return cachedCommand;
3333
- }
3334
- throw new MountError(
3335
- "MOUNT_SERVER_BIN_MISSING",
3336
- `mount-server bin not found at ${distPath} or ${srcPath}. Run pnpm build, or set MACHINEN_MOUNT_SERVER_BIN to an absolute path.`
3337
- );
3338
- }
3339
-
3340
3287
  // src/rootfs-img.ts
3341
3288
  import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "child_process";
3342
3289
  import { createHash } from "crypto";
3343
3290
  import {
3344
3291
  closeSync as closeSync2,
3345
- existsSync as existsSync8,
3292
+ existsSync as existsSync7,
3346
3293
  fsyncSync as fsyncSync2,
3347
3294
  mkdirSync as mkdirSync6,
3348
3295
  mkdtempSync as mkdtempSync2,
3349
3296
  openSync as openSync2,
3350
3297
  readSync,
3351
3298
  renameSync as renameSync3,
3352
- rmSync as rmSync5,
3299
+ rmSync as rmSync4,
3353
3300
  statSync as statSync4,
3354
3301
  truncateSync,
3355
3302
  unlinkSync as unlinkSync5,
@@ -3357,9 +3304,9 @@ import {
3357
3304
  } from "fs";
3358
3305
  import { createRequire as createRequire2 } from "module";
3359
3306
  import { arch, homedir as homedir5, platform as platform3 } from "os";
3360
- import { dirname as dirname6, join as join7, resolve } from "path";
3361
- import debugLib7 from "debug";
3362
- var debug7 = debugLib7("machinen:rootfs-img");
3307
+ import { join as join7, resolve } from "path";
3308
+ import debugLib6 from "debug";
3309
+ var debug6 = debugLib6("machinen:rootfs-img");
3363
3310
  function rootfsImgCacheDir() {
3364
3311
  return join7(homedir5(), ".cache", "machinen", "rootfs");
3365
3312
  }
@@ -3367,7 +3314,7 @@ function okMarkerPath(imgPath) {
3367
3314
  return `${imgPath}.ok`;
3368
3315
  }
3369
3316
  function markRootfsImageClean(imgPath) {
3370
- if (!existsSync8(imgPath)) {
3317
+ if (!existsSync7(imgPath)) {
3371
3318
  return;
3372
3319
  }
3373
3320
  const okPath = okMarkerPath(imgPath);
@@ -3380,7 +3327,7 @@ function markRootfsImageClean(imgPath) {
3380
3327
  fd = -1;
3381
3328
  renameSync3(tmp, okPath);
3382
3329
  } catch (err) {
3383
- debug7("markRootfsImageClean failed img=%s err=%s", imgPath, err.message);
3330
+ debug6("markRootfsImageClean failed img=%s err=%s", imgPath, err.message);
3384
3331
  if (fd >= 0) {
3385
3332
  try {
3386
3333
  closeSync2(fd);
@@ -3395,7 +3342,7 @@ function markRootfsImageClean(imgPath) {
3395
3342
  }
3396
3343
  function ensureRootfsImage(tarPath, opts = {}) {
3397
3344
  const tarAbs = resolve(tarPath);
3398
- if (!existsSync8(tarAbs)) {
3345
+ if (!existsSync7(tarAbs)) {
3399
3346
  throw new ProvisionError(
3400
3347
  "PROVISION_BASE_NOT_FOUND",
3401
3348
  `ensureRootfsImage: tarball not found at ${tarAbs}`
@@ -3408,10 +3355,10 @@ function ensureRootfsImage(tarPath, opts = {}) {
3408
3355
  opts.onPhase?.("sha256", Date.now() - shaT0);
3409
3356
  const imgPath = join7(cacheDir, `${sha}.img`);
3410
3357
  const okPath = okMarkerPath(imgPath);
3411
- if (!opts.force && existsSync8(imgPath)) {
3412
- debug7("cache hit sha=%s img=%s", sha.slice(0, 12), imgPath);
3413
- if (!existsSync8(okPath)) {
3414
- debug7("cache hit but no clean marker, will rematerialize img=%s", imgPath);
3358
+ if (!opts.force && existsSync7(imgPath)) {
3359
+ debug6("cache hit sha=%s img=%s", sha.slice(0, 12), imgPath);
3360
+ if (!existsSync7(okPath)) {
3361
+ debug6("cache hit but no clean marker, will rematerialize img=%s", imgPath);
3415
3362
  } else {
3416
3363
  try {
3417
3364
  unlinkSync5(okPath);
@@ -3427,21 +3374,21 @@ function ensureRootfsImage(tarPath, opts = {}) {
3427
3374
  const cur = statSync4(imgPath).size;
3428
3375
  if (opts.sizeBytes > cur) {
3429
3376
  truncateSync(imgPath, opts.sizeBytes);
3430
- debug7("cache hit grew img=%s from=%d to=%d", imgPath, cur, opts.sizeBytes);
3377
+ debug6("cache hit grew img=%s from=%d to=%d", imgPath, cur, opts.sizeBytes);
3431
3378
  }
3432
3379
  } catch (err) {
3433
- debug7("cache hit grow failed img=%s err=%s", imgPath, err.message);
3380
+ debug6("cache hit grow failed img=%s err=%s", imgPath, err.message);
3434
3381
  }
3435
3382
  opts.onPhase?.("sparse-extend", Date.now() - truncT0);
3436
3383
  }
3437
3384
  return imgPath;
3438
3385
  }
3439
- debug7("cache hit unusable, will rematerialize img=%s", imgPath);
3386
+ debug6("cache hit unusable, will rematerialize img=%s", imgPath);
3440
3387
  }
3441
3388
  }
3442
3389
  if (!opts.force) {
3443
3390
  const sibling = siblingPrebakePath(tarAbs);
3444
- if (sibling && existsSync8(sibling)) {
3391
+ if (sibling && existsSync7(sibling)) {
3445
3392
  const prebakeT0 = Date.now();
3446
3393
  const fast = tryPrebakeFromSibling({
3447
3394
  sibling,
@@ -3469,7 +3416,7 @@ function ensureRootfsImage(tarPath, opts = {}) {
3469
3416
  const stagingImg = join7(stagingDir, "rootfs.img");
3470
3417
  mkdirSync6(stagingTree, { recursive: true });
3471
3418
  try {
3472
- debug7("materialize sha=%s tar=%s", sha.slice(0, 12), tarAbs);
3419
+ debug6("materialize sha=%s tar=%s", sha.slice(0, 12), tarAbs);
3473
3420
  const extractT0 = Date.now();
3474
3421
  extractTarball(tarAbs, stagingTree);
3475
3422
  opts.onPhase?.("tar-extract", Date.now() - extractT0);
@@ -3477,7 +3424,7 @@ function ensureRootfsImage(tarPath, opts = {}) {
3477
3424
  const multiplier = opts.sizeMultiplier ?? 2.5;
3478
3425
  const minBytes = opts.minSizeBytes ?? 2 * 1024 * 1024 * 1024;
3479
3426
  const sizeBytes = opts.sizeBytes ?? Math.max(minBytes, Math.ceil(treeBytes * multiplier));
3480
- debug7(
3427
+ debug6(
3481
3428
  "size tree=%d size=%d multiplier=%s explicit=%s",
3482
3429
  treeBytes,
3483
3430
  sizeBytes,
@@ -3500,11 +3447,11 @@ function ensureRootfsImage(tarPath, opts = {}) {
3500
3447
  );
3501
3448
  }
3502
3449
  renameSync3(stagingImg, imgPath);
3503
- debug7("materialize done sha=%s img=%s sizeBytes=%d", sha.slice(0, 12), imgPath, sizeBytes);
3450
+ debug6("materialize done sha=%s img=%s sizeBytes=%d", sha.slice(0, 12), imgPath, sizeBytes);
3504
3451
  return imgPath;
3505
3452
  } finally {
3506
3453
  try {
3507
- rmSync5(stagingDir, { recursive: true, force: true });
3454
+ rmSync4(stagingDir, { recursive: true, force: true });
3508
3455
  } catch {
3509
3456
  }
3510
3457
  }
@@ -3515,7 +3462,7 @@ function cachedImageIsUsable(imgPath) {
3515
3462
  }
3516
3463
  const e2fsck = whichFirst(["e2fsck"]);
3517
3464
  if (!e2fsck) {
3518
- debug7("no e2fsck on PATH; trusting cached img=%s", imgPath);
3465
+ debug6("no e2fsck on PATH; trusting cached img=%s", imgPath);
3519
3466
  return true;
3520
3467
  }
3521
3468
  const r = spawnSync2(e2fsck, ["-fy", imgPath], {
@@ -3524,7 +3471,7 @@ function cachedImageIsUsable(imgPath) {
3524
3471
  if (r.status === 0 || r.status === 1 || r.status === 2) {
3525
3472
  return true;
3526
3473
  }
3527
- debug7(
3474
+ debug6(
3528
3475
  "e2fsck rejected img=%s status=%s stderr=%s",
3529
3476
  imgPath,
3530
3477
  r.status,
@@ -3595,7 +3542,7 @@ function findKegOnlyE2fs(names, dirs = KEG_ONLY_E2FS_DIRS) {
3595
3542
  for (const dir of dirs) {
3596
3543
  for (const name of names) {
3597
3544
  const candidate = join7(dir, name);
3598
- if (existsSync8(candidate)) {
3545
+ if (existsSync7(candidate)) {
3599
3546
  return candidate;
3600
3547
  }
3601
3548
  }
@@ -3607,8 +3554,8 @@ function resolveMke2fsEnvOverride() {
3607
3554
  if (!envOverride) {
3608
3555
  return void 0;
3609
3556
  }
3610
- if (existsSync8(envOverride)) {
3611
- debug7("resolved via MACHINEN_MKE2FS=%s", envOverride);
3557
+ if (existsSync7(envOverride)) {
3558
+ debug6("resolved via MACHINEN_MKE2FS=%s", envOverride);
3612
3559
  return envOverride;
3613
3560
  }
3614
3561
  throw new ProvisionError(
@@ -3618,12 +3565,11 @@ function resolveMke2fsEnvOverride() {
3618
3565
  }
3619
3566
  var require_2 = createRequire2(import.meta.url);
3620
3567
  function findBundledMke2fs() {
3621
- const pkg = `@machinen/e2fsprogs-${arch()}-${platform3()}`;
3568
+ const pkg = `@machinen/native-${arch()}-${platform3()}`;
3622
3569
  try {
3623
- const pkgJson = require_2.resolve(`${pkg}/package.json`);
3624
- const candidate = join7(dirname6(pkgJson), "bin", "mke2fs");
3625
- if (existsSync8(candidate)) {
3626
- return candidate;
3570
+ const mod = require_2(pkg);
3571
+ if (mod.mke2fs && existsSync7(mod.mke2fs)) {
3572
+ return mod.mke2fs;
3627
3573
  }
3628
3574
  } catch {
3629
3575
  }
@@ -3652,7 +3598,7 @@ function tryPrebakeFromSibling(args) {
3652
3598
  const stagingDir = mkdtempSync2(join7(cacheDir, `${sha.slice(0, 12)}-prebake-`));
3653
3599
  const stagingImg = join7(stagingDir, "rootfs.img");
3654
3600
  try {
3655
- debug7("prebake try sibling=%s sha=%s", sibling, sha.slice(0, 12));
3601
+ debug6("prebake try sibling=%s sha=%s", sibling, sha.slice(0, 12));
3656
3602
  const dstFd = openSync2(stagingImg, "w");
3657
3603
  let gunzipOk = false;
3658
3604
  try {
@@ -3661,7 +3607,7 @@ function tryPrebakeFromSibling(args) {
3661
3607
  });
3662
3608
  gunzipOk = r.status === 0;
3663
3609
  if (!gunzipOk) {
3664
- debug7(
3610
+ debug6(
3665
3611
  "prebake gunzip failed sibling=%s status=%s stderr=%s",
3666
3612
  sibling,
3667
3613
  r.status,
@@ -3675,7 +3621,7 @@ function tryPrebakeFromSibling(args) {
3675
3621
  return void 0;
3676
3622
  }
3677
3623
  if (!looksLikeExt4(stagingImg)) {
3678
- debug7("prebake sibling did not yield an ext4 image \u2014 falling back");
3624
+ debug6("prebake sibling did not yield an ext4 image \u2014 falling back");
3679
3625
  return void 0;
3680
3626
  }
3681
3627
  if (sizeBytes !== void 0) {
@@ -3685,14 +3631,14 @@ function tryPrebakeFromSibling(args) {
3685
3631
  }
3686
3632
  }
3687
3633
  renameSync3(stagingImg, imgPath);
3688
- debug7("prebake done sha=%s img=%s", sha.slice(0, 12), imgPath);
3634
+ debug6("prebake done sha=%s img=%s", sha.slice(0, 12), imgPath);
3689
3635
  return imgPath;
3690
3636
  } catch (err) {
3691
- debug7("prebake error sibling=%s err=%s", sibling, err.message);
3637
+ debug6("prebake error sibling=%s err=%s", sibling, err.message);
3692
3638
  return void 0;
3693
3639
  } finally {
3694
3640
  try {
3695
- rmSync5(stagingDir, { recursive: true, force: true });
3641
+ rmSync4(stagingDir, { recursive: true, force: true });
3696
3642
  } catch {
3697
3643
  }
3698
3644
  }
@@ -3727,7 +3673,7 @@ function prebakeRootfsImageFromTree(args) {
3727
3673
  try {
3728
3674
  const mke2fs = resolveMke2fs();
3729
3675
  if (!mke2fs) {
3730
- debug7("prebake skip: no mke2fs available");
3676
+ debug6("prebake skip: no mke2fs available");
3731
3677
  return;
3732
3678
  }
3733
3679
  mkdirSync6(cacheDir, { recursive: true });
@@ -3735,8 +3681,8 @@ function prebakeRootfsImageFromTree(args) {
3735
3681
  const sha = sha256OfFile(tarPath);
3736
3682
  onPhase?.("prebake.sha256", Date.now() - shaT0);
3737
3683
  const imgPath = join7(cacheDir, `${sha}.img`);
3738
- if (existsSync8(imgPath)) {
3739
- debug7("prebake skip: cache already populated sha=%s", sha.slice(0, 12));
3684
+ if (existsSync7(imgPath)) {
3685
+ debug6("prebake skip: cache already populated sha=%s", sha.slice(0, 12));
3740
3686
  return;
3741
3687
  }
3742
3688
  const stagingDir = mkdtempSync2(join7(cacheDir, `${sha.slice(0, 12)}-prebake-tree-`));
@@ -3754,7 +3700,7 @@ function prebakeRootfsImageFromTree(args) {
3754
3700
  );
3755
3701
  onPhase?.("prebake.mke2fs", Date.now() - mkT0);
3756
3702
  if (mk.status !== 0) {
3757
- debug7(
3703
+ debug6(
3758
3704
  "prebake mke2fs failed status=%s stderr=%s",
3759
3705
  mk.status,
3760
3706
  mk.stderr?.toString().slice(0, 200) ?? ""
@@ -3763,21 +3709,21 @@ function prebakeRootfsImageFromTree(args) {
3763
3709
  }
3764
3710
  renameSync3(stagingImg, imgPath);
3765
3711
  markRootfsImageClean(imgPath);
3766
- debug7("prebake emitted cache=%s sizeBytes=%d", imgPath, sizeBytes);
3712
+ debug6("prebake emitted cache=%s sizeBytes=%d", imgPath, sizeBytes);
3767
3713
  } finally {
3768
3714
  try {
3769
- rmSync5(stagingDir, { recursive: true, force: true });
3715
+ rmSync4(stagingDir, { recursive: true, force: true });
3770
3716
  } catch {
3771
3717
  }
3772
3718
  }
3773
3719
  } catch (err) {
3774
- debug7("prebake error err=%s", err.message);
3720
+ debug6("prebake error err=%s", err.message);
3775
3721
  }
3776
3722
  }
3777
3723
 
3778
3724
  // src/vm/bundle.ts
3779
3725
  import { randomBytes as randomBytes2 } from "crypto";
3780
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync5, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
3726
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync5, rmSync as rmSync7, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
3781
3727
  import { tmpdir as tmpdir4 } from "os";
3782
3728
  import { join as join11, resolve as resolve4 } from "path";
3783
3729
 
@@ -3785,24 +3731,24 @@ import { join as join11, resolve as resolve4 } from "path";
3785
3731
  import { spawnSync as spawnSync3 } from "child_process";
3786
3732
  import {
3787
3733
  cpSync,
3788
- existsSync as existsSync9,
3734
+ existsSync as existsSync8,
3789
3735
  lstatSync,
3790
3736
  mkdirSync as mkdirSync7,
3791
3737
  mkdtempSync as mkdtempSync3,
3792
3738
  readdirSync as readdirSync4,
3793
- readFileSync as readFileSync8,
3739
+ readFileSync as readFileSync7,
3794
3740
  readlinkSync as readlinkSync2,
3795
- rmSync as rmSync6,
3741
+ rmSync as rmSync5,
3796
3742
  statSync as statSync5,
3797
3743
  writeFileSync as writeFileSync5
3798
3744
  } from "fs";
3799
3745
  import { createRequire as createRequire3 } from "module";
3800
3746
  import { arch as osArch2, platform as osPlatform5, tmpdir as tmpdir2 } from "os";
3801
- import { dirname as dirname7, join as join8 } from "path";
3802
- import { fileURLToPath as fileURLToPath2 } from "url";
3803
- import debugLib8 from "debug";
3747
+ import { dirname as dirname6, join as join8 } from "path";
3748
+ import { fileURLToPath } from "url";
3749
+ import debugLib7 from "debug";
3804
3750
  var require_3 = createRequire3(import.meta.url);
3805
- var debug8 = debugLib8("machinen:mkinitramfs");
3751
+ var debug7 = debugLib7("machinen:mkinitramfs");
3806
3752
  var DEFAULT_WORKSPACE_EXCLUDES = /* @__PURE__ */ new Set([
3807
3753
  ".git",
3808
3754
  "node_modules",
@@ -3862,7 +3808,7 @@ function newc(name, mode, opts = {}) {
3862
3808
  return out;
3863
3809
  }
3864
3810
  function loadExcludes(path) {
3865
- const raw = readFileSync8(path, "utf8");
3811
+ const raw = readFileSync7(path, "utf8");
3866
3812
  const out = [];
3867
3813
  for (const line of raw.split("\n")) {
3868
3814
  const stripped = line.split("#", 1)[0].trim();
@@ -3955,7 +3901,7 @@ function* walkRootfs(root, rel, excludes, counts) {
3955
3901
  yield newc(childRel, 16384 | m & 4095);
3956
3902
  yield* walkRootfs(root, childRel, excludes, counts);
3957
3903
  } else if (st.isFile()) {
3958
- yield newc(childRel, 32768 | m & 4095, { data: readFileSync8(childFull) });
3904
+ yield newc(childRel, 32768 | m & 4095, { data: readFileSync7(childFull) });
3959
3905
  }
3960
3906
  }
3961
3907
  }
@@ -3992,7 +3938,7 @@ function* walkWorkspace(root, rel, mountpoint, excludes, counts) {
3992
3938
  yield newc(arcName, 16384 | m & 4095);
3993
3939
  yield* walkWorkspace(root, childRel, mountpoint, excludes, counts);
3994
3940
  } else if (st.isFile()) {
3995
- const data = readFileSync8(childFull);
3941
+ const data = readFileSync7(childFull);
3996
3942
  counts.bytes += data.length;
3997
3943
  yield newc(arcName, 32768 | m & 4095, { data });
3998
3944
  }
@@ -4009,7 +3955,7 @@ function packBundle(opts) {
4009
3955
  throw new MkinitramfsError("MKINITRAMFS_BUNDLE_INVALID", `--bundle: missing ${cfgPath}`);
4010
3956
  }
4011
3957
  const needsMerge = Boolean(opts.base) || Boolean(opts.mount);
4012
- debug8(
3958
+ debug7(
4013
3959
  "packBundle bundle=%s out=%s base=%s mount=%s needsMerge=%s",
4014
3960
  opts.bundle,
4015
3961
  opts.out,
@@ -4025,13 +3971,13 @@ function packBundle(opts) {
4025
3971
  const extractT0 = Date.now();
4026
3972
  const res = spawnSync3("tar", ["-xzf", opts.base, "-C", mergeTmp]);
4027
3973
  if (res.status !== 0) {
4028
- rmSync6(mergeTmp, { recursive: true, force: true });
3974
+ rmSync5(mergeTmp, { recursive: true, force: true });
4029
3975
  throw new MkinitramfsError(
4030
3976
  "MKINITRAMFS_BASE_EXTRACT_FAILED",
4031
3977
  `tar -xzf ${opts.base} failed: ${res.stderr?.toString() ?? ""}`
4032
3978
  );
4033
3979
  }
4034
- debug8("base extracted elapsed=%dms", Date.now() - extractT0);
3980
+ debug7("base extracted elapsed=%dms", Date.now() - extractT0);
4035
3981
  }
4036
3982
  if (opts.mount) {
4037
3983
  overlayMount(mergeTmp, opts.mount.host, opts.mount.guest);
@@ -4051,8 +3997,7 @@ function packBundle(opts) {
4051
3997
  }
4052
3998
  appendFinalEntries(parts, {
4053
3999
  initPath: opts.initPath ?? defaultInitPath(),
4054
- config: patchConfigEnv(readFileSync8(cfgPath), opts.env),
4055
- fuseAgentPath: opts.fuseAgentPath,
4000
+ config: patchConfigEnv(readFileSync7(cfgPath), opts.env),
4056
4001
  // Always inject the fresh /init AFTER walking the rootfs.
4057
4002
  // `provision()` flows feed the previous run's frozen rootfs back
4058
4003
  // in as `base`, and that capture has /init at root. The walked
@@ -4073,7 +4018,7 @@ function packBundle(opts) {
4073
4018
  execAgentPath: opts.execAgentPath ?? defaultExecAgentPath()
4074
4019
  });
4075
4020
  writeFileSync5(opts.out, Buffer.concat(parts));
4076
- debug8(
4021
+ debug7(
4077
4022
  "packBundle done files=%d bytes=%d elapsed=%dms",
4078
4023
  counts.files,
4079
4024
  counts.bytes,
@@ -4081,7 +4026,7 @@ function packBundle(opts) {
4081
4026
  );
4082
4027
  } finally {
4083
4028
  if (mergeTmp) {
4084
- rmSync6(mergeTmp, { recursive: true, force: true });
4029
+ rmSync5(mergeTmp, { recursive: true, force: true });
4085
4030
  }
4086
4031
  }
4087
4032
  }
@@ -4097,7 +4042,7 @@ function patchConfigEnv(config, env) {
4097
4042
  function overlayMount(mergeRoot, hostAbs, guest) {
4098
4043
  const rel = guest.replace(/^\/+/, "");
4099
4044
  const dst = join8(mergeRoot, rel);
4100
- mkdirSync7(dirname7(dst), { recursive: true });
4045
+ mkdirSync7(dirname6(dst), { recursive: true });
4101
4046
  cpSync(hostAbs, dst, {
4102
4047
  recursive: true,
4103
4048
  force: true,
@@ -4114,13 +4059,12 @@ function packTinyBundle(opts) {
4114
4059
  parts.push(newc(".", 16877));
4115
4060
  appendFinalEntries(parts, {
4116
4061
  initPath: opts.initPath ?? defaultInitPath(),
4117
- config: patchConfigEnv(readFileSync8(cfgPath), opts.env),
4118
- fuseAgentPath: opts.fuseAgentPath,
4062
+ config: patchConfigEnv(readFileSync7(cfgPath), opts.env),
4119
4063
  injectInit: true,
4120
4064
  mountGuest: opts.mountGuest
4121
4065
  });
4122
4066
  writeFileSync5(opts.out, Buffer.concat(parts));
4123
- debug8("packTinyBundle done elapsed=%dms", Date.now() - t0);
4067
+ debug7("packTinyBundle done elapsed=%dms", Date.now() - t0);
4124
4068
  }
4125
4069
  function packRootfs(opts) {
4126
4070
  const counts = { files: 0, bytes: 0 };
@@ -4130,7 +4074,7 @@ function packRootfs(opts) {
4130
4074
  }
4131
4075
  appendFinalEntries(parts, {
4132
4076
  initPath: opts.initPath ?? defaultInitPath(),
4133
- config: opts.config ? readFileSync8(opts.config) : void 0,
4077
+ config: opts.config ? readFileSync7(opts.config) : void 0,
4134
4078
  injectInit: true
4135
4079
  });
4136
4080
  writeFileSync5(opts.out, Buffer.concat(parts));
@@ -4140,11 +4084,11 @@ function packMinimal(opts) {
4140
4084
  const parts = [
4141
4085
  newc(".", 16877),
4142
4086
  newc("dev", 16877),
4143
- newc("init", 33261, { data: readFileSync8(initPath) })
4087
+ newc("init", 33261, { data: readFileSync7(initPath) })
4144
4088
  ];
4145
4089
  appendFinalEntries(parts, {
4146
4090
  initPath,
4147
- config: opts.config ? readFileSync8(opts.config) : void 0,
4091
+ config: opts.config ? readFileSync7(opts.config) : void 0,
4148
4092
  injectInit: true
4149
4093
  });
4150
4094
  writeFileSync5(opts.out, Buffer.concat(parts));
@@ -4180,7 +4124,7 @@ function appendFinalEntries(parts, opts) {
4180
4124
  if (opts.injectInit) {
4181
4125
  let initBytes = null;
4182
4126
  try {
4183
- initBytes = readFileSync8(opts.initPath);
4127
+ initBytes = readFileSync7(opts.initPath);
4184
4128
  } catch (err) {
4185
4129
  if (process.env.MACHINEN_REQUIRE_FIXTURES !== "0") {
4186
4130
  throw new MkinitramfsError(
@@ -4196,18 +4140,11 @@ function appendFinalEntries(parts, opts) {
4196
4140
  }
4197
4141
  if (opts.execAgentPath) {
4198
4142
  try {
4199
- const bytes = readFileSync8(opts.execAgentPath);
4143
+ const bytes = readFileSync7(opts.execAgentPath);
4200
4144
  parts.push(newc("exec-agent", 33261, { data: bytes }));
4201
4145
  } catch {
4202
4146
  }
4203
4147
  }
4204
- if (opts.fuseAgentPath) {
4205
- try {
4206
- const bytes = readFileSync8(opts.fuseAgentPath);
4207
- parts.push(newc("fuse-agent", 33261, { data: bytes }));
4208
- } catch {
4209
- }
4210
- }
4211
4148
  if (opts.config) {
4212
4149
  parts.push(newc("machinen-config.json", 33188, { data: opts.config }));
4213
4150
  }
@@ -4234,24 +4171,22 @@ function resolveGuestPaths() {
4234
4171
  if (cachedGuestPaths) {
4235
4172
  return cachedGuestPaths;
4236
4173
  }
4237
- const pkgName = `@machinen/vmm-${osArch2()}-${osPlatform5()}`;
4174
+ const pkgName = `@machinen/native-${osArch2()}-${osPlatform5()}`;
4238
4175
  try {
4239
4176
  const mod = require_3(pkgName);
4240
- if (mod.initPath && mod.fuseAgentPath && mod.execAgentPath && existsSync9(mod.initPath)) {
4177
+ if (mod.initPath && mod.execAgentPath && existsSync8(mod.initPath)) {
4241
4178
  cachedGuestPaths = {
4242
4179
  initPath: mod.initPath,
4243
- fuseAgentPath: mod.fuseAgentPath,
4244
4180
  execAgentPath: mod.execAgentPath
4245
4181
  };
4246
4182
  return cachedGuestPaths;
4247
4183
  }
4248
4184
  } catch {
4249
4185
  }
4250
- const here = dirname7(fileURLToPath2(import.meta.url));
4186
+ const here = dirname6(fileURLToPath(import.meta.url));
4251
4187
  const fixtures = join8(here, "..", "..", "microvm", "test-fixtures");
4252
4188
  cachedGuestPaths = {
4253
4189
  initPath: join8(fixtures, "init"),
4254
- fuseAgentPath: join8(fixtures, "fuse-agent"),
4255
4190
  execAgentPath: join8(fixtures, "exec-agent")
4256
4191
  };
4257
4192
  return cachedGuestPaths;
@@ -4259,9 +4194,6 @@ function resolveGuestPaths() {
4259
4194
  function defaultInitPath() {
4260
4195
  return resolveGuestPaths().initPath;
4261
4196
  }
4262
- function defaultFuseAgentPath() {
4263
- return resolveGuestPaths().fuseAgentPath;
4264
- }
4265
4197
  function defaultExecAgentPath() {
4266
4198
  return resolveGuestPaths().execAgentPath;
4267
4199
  }
@@ -4357,7 +4289,7 @@ function takeFlag(args, flag) {
4357
4289
  return value;
4358
4290
  }
4359
4291
  function defaultOut() {
4360
- const here = dirname7(fileURLToPath2(import.meta.url));
4292
+ const here = dirname6(fileURLToPath(import.meta.url));
4361
4293
  return join8(here, "..", "..", "microvm", "test-fixtures", "initramfs.cpio");
4362
4294
  }
4363
4295
  function die(msg) {
@@ -4370,7 +4302,7 @@ import { execFileSync as execFileSync6, spawnSync as spawnSync4 } from "child_pr
4370
4302
  import { createHash as createHash2 } from "crypto";
4371
4303
  import {
4372
4304
  closeSync as closeSync3,
4373
- existsSync as existsSync10,
4305
+ existsSync as existsSync9,
4374
4306
  fsyncSync as fsyncSync3,
4375
4307
  mkdirSync as mkdirSync8,
4376
4308
  mkdtempSync as mkdtempSync4,
@@ -4379,7 +4311,7 @@ import {
4379
4311
  readdirSync as readdirSync5,
4380
4312
  readlinkSync as readlinkSync3,
4381
4313
  renameSync as renameSync4,
4382
- rmSync as rmSync7,
4314
+ rmSync as rmSync6,
4383
4315
  statSync as statSync6,
4384
4316
  truncateSync as truncateSync2,
4385
4317
  unlinkSync as unlinkSync6,
@@ -4388,9 +4320,9 @@ import {
4388
4320
  } from "fs";
4389
4321
  import { createRequire as createRequire4 } from "module";
4390
4322
  import { arch as arch2, homedir as homedir6, platform as platform4, tmpdir as tmpdir3 } from "os";
4391
- import { dirname as dirname8, join as join9, resolve as resolve2 } from "path";
4392
- import debugLib9 from "debug";
4393
- var debug9 = debugLib9("machinen:mountdisk-img");
4323
+ import { join as join9, resolve as resolve2 } from "path";
4324
+ import debugLib8 from "debug";
4325
+ var debug8 = debugLib8("machinen:mountdisk-img");
4394
4326
  function mountdiskImgCacheDir() {
4395
4327
  return join9(homedir6(), ".cache", "machinen", "mountdisk");
4396
4328
  }
@@ -4398,7 +4330,7 @@ function okMarkerPath2(imgPath) {
4398
4330
  return `${imgPath}.ok`;
4399
4331
  }
4400
4332
  function markMountDiskImageClean(imgPath) {
4401
- if (!existsSync10(imgPath)) {
4333
+ if (!existsSync9(imgPath)) {
4402
4334
  return;
4403
4335
  }
4404
4336
  const okPath = okMarkerPath2(imgPath);
@@ -4411,7 +4343,7 @@ function markMountDiskImageClean(imgPath) {
4411
4343
  fd = -1;
4412
4344
  renameSync4(tmp, okPath);
4413
4345
  } catch (err) {
4414
- debug9("markMountDiskImageClean failed img=%s err=%s", imgPath, err.message);
4346
+ debug8("markMountDiskImageClean failed img=%s err=%s", imgPath, err.message);
4415
4347
  if (fd >= 0) {
4416
4348
  try {
4417
4349
  closeSync3(fd);
@@ -4426,7 +4358,7 @@ function markMountDiskImageClean(imgPath) {
4426
4358
  }
4427
4359
  function ensureMountDiskImage(hostAbs, opts = {}) {
4428
4360
  const hostResolved = resolve2(hostAbs);
4429
- if (!existsSync10(hostResolved)) {
4361
+ if (!existsSync9(hostResolved)) {
4430
4362
  throw new BootError(
4431
4363
  "BOOT_MOUNT_HOST_NOT_FOUND",
4432
4364
  `ensureMountDiskImage: host directory not found at ${hostResolved}`
@@ -4445,10 +4377,10 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4445
4377
  opts.onPhase?.("manifest-hash", Date.now() - hashT0);
4446
4378
  const imgPath = join9(cacheDir, `${key}.sqfs`);
4447
4379
  const okPath = okMarkerPath2(imgPath);
4448
- if (!opts.force && existsSync10(imgPath)) {
4449
- debug9("cache hit key=%s img=%s", key.slice(0, 12), imgPath);
4450
- if (!existsSync10(okPath)) {
4451
- debug9("cache hit but no clean marker \u2014 rematerialising img=%s", imgPath);
4380
+ if (!opts.force && existsSync9(imgPath)) {
4381
+ debug8("cache hit key=%s img=%s", key.slice(0, 12), imgPath);
4382
+ if (!existsSync9(okPath)) {
4383
+ debug8("cache hit but no clean marker \u2014 rematerialising img=%s", imgPath);
4452
4384
  } else {
4453
4385
  try {
4454
4386
  unlinkSync6(okPath);
@@ -4467,7 +4399,7 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4467
4399
  const stagingDir = mkdtempSync4(join9(cacheDir, `${key.slice(0, 12)}-staging-`));
4468
4400
  const stagingImg = join9(stagingDir, "lower.sqfs");
4469
4401
  try {
4470
- debug9("materialize key=%s host=%s", key.slice(0, 12), hostResolved);
4402
+ debug8("materialize key=%s host=%s", key.slice(0, 12), hostResolved);
4471
4403
  const mkT0 = Date.now();
4472
4404
  const args = [
4473
4405
  hostResolved,
@@ -4504,11 +4436,11 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4504
4436
  padTo512Boundary(stagingImg);
4505
4437
  renameSync4(stagingImg, imgPath);
4506
4438
  opts.onPhase?.("staging-rename", Date.now() - renameT0);
4507
- debug9("materialize done key=%s img=%s", key.slice(0, 12), imgPath);
4439
+ debug8("materialize done key=%s img=%s", key.slice(0, 12), imgPath);
4508
4440
  return { lowerPath: imgPath, key };
4509
4441
  } finally {
4510
4442
  try {
4511
- rmSync7(stagingDir, { recursive: true, force: true });
4443
+ rmSync6(stagingDir, { recursive: true, force: true });
4512
4444
  } catch {
4513
4445
  }
4514
4446
  }
@@ -4554,8 +4486,8 @@ function resolveMksquashfsEnvOverride() {
4554
4486
  if (!envOverride) {
4555
4487
  return void 0;
4556
4488
  }
4557
- if (existsSync10(envOverride)) {
4558
- debug9("resolved via MACHINEN_MKSQUASHFS=%s", envOverride);
4489
+ if (existsSync9(envOverride)) {
4490
+ debug8("resolved via MACHINEN_MKSQUASHFS=%s", envOverride);
4559
4491
  return envOverride;
4560
4492
  }
4561
4493
  throw new BootError(
@@ -4565,12 +4497,11 @@ function resolveMksquashfsEnvOverride() {
4565
4497
  }
4566
4498
  var require_4 = createRequire4(import.meta.url);
4567
4499
  function findBundledMksquashfs() {
4568
- const pkg = `@machinen/squashfs-tools-${arch2()}-${platform4()}`;
4500
+ const pkg = `@machinen/native-${arch2()}-${platform4()}`;
4569
4501
  try {
4570
- const pkgJson = require_4.resolve(`${pkg}/package.json`);
4571
- const candidate = join9(dirname8(pkgJson), "bin", "mksquashfs");
4572
- if (existsSync10(candidate)) {
4573
- return candidate;
4502
+ const mod = require_4(pkg);
4503
+ if (mod.mksquashfs && existsSync9(mod.mksquashfs)) {
4504
+ return mod.mksquashfs;
4574
4505
  }
4575
4506
  } catch {
4576
4507
  }
@@ -4587,7 +4518,7 @@ var KEG_ONLY_DIRS = [
4587
4518
  function findKegOnlyMksquashfs() {
4588
4519
  for (const dir of KEG_ONLY_DIRS) {
4589
4520
  const candidate = join9(dir, "mksquashfs");
4590
- if (existsSync10(candidate)) {
4521
+ if (existsSync9(candidate)) {
4591
4522
  return candidate;
4592
4523
  }
4593
4524
  }
@@ -4697,12 +4628,12 @@ function randomSuffix() {
4697
4628
 
4698
4629
  // src/vm/helpers.ts
4699
4630
  import { randomBytes } from "crypto";
4700
- import { closeSync as closeSync4, existsSync as existsSync11, openSync as openSync4, writeSync as writeSync3 } from "fs";
4631
+ import { closeSync as closeSync4, existsSync as existsSync10, openSync as openSync4, writeSync as writeSync3 } from "fs";
4701
4632
  import { createRequire as createRequire5 } from "module";
4702
4633
  import { arch as osArch3, platform as osPlatform6, totalmem as totalmem2 } from "os";
4703
4634
  import { resolve as resolve3 } from "path";
4704
- import debugLib10 from "debug";
4705
- var debug10 = debugLib10("machinen:boot");
4635
+ import debugLib9 from "debug";
4636
+ var debug9 = debugLib9("machinen:boot");
4706
4637
  var require_5 = createRequire5(import.meta.url);
4707
4638
  var SNAP_SCRATCH_BYTES = 8 * 1024 * 1024 * 1024;
4708
4639
  function allocateSparseFile3(path, sizeBytes) {
@@ -4740,7 +4671,7 @@ function resolveVmmBinary() {
4740
4671
  const envOverride = process.env.MACHINEN_VMM;
4741
4672
  if (envOverride) {
4742
4673
  const abs = resolve3(envOverride);
4743
- if (!existsSync11(abs)) {
4674
+ if (!existsSync10(abs)) {
4744
4675
  throw new BootError(
4745
4676
  "BOOT_VMM_MISSING",
4746
4677
  `MACHINEN_VMM is set to ${envOverride}, but that file does not exist.`
@@ -4749,13 +4680,13 @@ function resolveVmmBinary() {
4749
4680
  return abs;
4750
4681
  }
4751
4682
  const key = `${osArch3()}-${osPlatform6()}`;
4752
- const pkgName = `@machinen/vmm-${key}`;
4683
+ const pkgName = `@machinen/native-${key}`;
4753
4684
  try {
4754
4685
  const mod = require_5(pkgName);
4755
- if (!mod.binary || !existsSync11(mod.binary)) {
4686
+ if (!mod.binary || !existsSync10(mod.binary)) {
4756
4687
  throw new BootError(
4757
4688
  "BOOT_VMM_PACKAGE_BROKEN",
4758
- `${pkgName} is installed but its binary is missing at ${mod.binary}.`
4689
+ `${pkgName} is installed but its VMM binary is missing at ${mod.binary}.`
4759
4690
  );
4760
4691
  }
4761
4692
  return mod.binary;
@@ -4911,7 +4842,7 @@ function buildGuestHostname(pid, name) {
4911
4842
  }
4912
4843
  async function setGuestHostname(vm, hostname) {
4913
4844
  if (hostname.length === 0 || hostname.includes("'") || hostname.includes("\n") || hostname.includes("\0")) {
4914
- debug10("setGuestHostname: refusing unsafe hostname %j", hostname);
4845
+ debug9("setGuestHostname: refusing unsafe hostname %j", hostname);
4915
4846
  return;
4916
4847
  }
4917
4848
  try {
@@ -4919,9 +4850,9 @@ async function setGuestHostname(vm, hostname) {
4919
4850
  connectTimeoutMs: 5e3,
4920
4851
  execTimeoutMs: 5e3
4921
4852
  });
4922
- debug10("setGuestHostname: set pid=%d hostname=%s", vm.pid, hostname);
4853
+ debug9("setGuestHostname: set pid=%d hostname=%s", vm.pid, hostname);
4923
4854
  } catch (err) {
4924
- debug10(
4855
+ debug9(
4925
4856
  "setGuestHostname: failed pid=%d hostname=%s err=%s",
4926
4857
  vm.pid,
4927
4858
  hostname,
@@ -4932,7 +4863,7 @@ async function setGuestHostname(vm, hostname) {
4932
4863
 
4933
4864
  // src/vm/image-config.ts
4934
4865
  import { execFileSync as execFileSync7 } from "child_process";
4935
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync6 } from "fs";
4866
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync8, statSync as statSync7, writeFileSync as writeFileSync6 } from "fs";
4936
4867
  import { homedir as homedir7 } from "os";
4937
4868
  import { join as join10 } from "path";
4938
4869
  function imageConfigCacheDir() {
@@ -4962,9 +4893,9 @@ function warmImageConfigCache(imagePath, config) {
4962
4893
  }
4963
4894
  function readImageConfig(imagePath) {
4964
4895
  const cachePath = imageConfigCachePath(imagePath);
4965
- if (cachePath && existsSync12(cachePath)) {
4896
+ if (cachePath && existsSync11(cachePath)) {
4966
4897
  try {
4967
- const raw = readFileSync9(cachePath, "utf8");
4898
+ const raw = readFileSync8(cachePath, "utf8");
4968
4899
  const cached = JSON.parse(raw);
4969
4900
  return cached.config ?? void 0;
4970
4901
  } catch {
@@ -4992,12 +4923,18 @@ function readImageConfig(imagePath) {
4992
4923
  }
4993
4924
 
4994
4925
  // src/vm/bundle.ts
4995
- var LIVE_MOUNT_PORT_BASE = 1970;
4996
- function resolveLiveMounts(mounts, cwd, udsDir) {
4926
+ var MAX_LIVE_MOUNTS = 5;
4927
+ function resolveLiveMounts(mounts, cwd) {
4928
+ if (mounts.length > MAX_LIVE_MOUNTS) {
4929
+ throw new BootError(
4930
+ "BOOT_MOUNT_INVALID",
4931
+ `liveMounts: at most ${MAX_LIVE_MOUNTS} live mounts are supported per VM (got ${mounts.length}) \u2014 the VMM wires ${MAX_LIVE_MOUNTS} virtio-fs slots.`
4932
+ );
4933
+ }
4997
4934
  return mounts.map((m, i) => {
4998
4935
  validateMountGuest(m.guest);
4999
4936
  const hostAbs = resolve4(cwd ?? process.cwd(), m.host);
5000
- if (!existsSync13(hostAbs)) {
4937
+ if (!existsSync12(hostAbs)) {
5001
4938
  throw new BootError(
5002
4939
  "BOOT_MOUNT_HOST_NOT_FOUND",
5003
4940
  `liveMounts[${i}] host path not found: ${m.host}`
@@ -5012,10 +4949,10 @@ function resolveLiveMounts(mounts, cwd, udsDir) {
5012
4949
  return {
5013
4950
  host: hostAbs,
5014
4951
  guest: normalizeMountGuest(m.guest),
5015
- port: LIVE_MOUNT_PORT_BASE + i,
5016
- udsPath: join11(udsDir, `live-mount-${i}.sock`),
5017
- statsPath: join11(udsDir, `live-mount-${i}-stats.json`),
5018
- mode: m.mode ?? "rw"
4952
+ mode: m.mode ?? "rw",
4953
+ // Tag is the virtio-fs device's config-space identifier and must
4954
+ // be ≤ 36 bytes (FsConfig.tag). `machinen-lm<i>` stays well under.
4955
+ tag: `machinen-lm${i}`
5019
4956
  };
5020
4957
  });
5021
4958
  }
@@ -5026,7 +4963,7 @@ function buildMachinenConfig(input) {
5026
4963
  cfg.cwd = effectiveCwd;
5027
4964
  }
5028
4965
  if (input.liveMounts.length > 0) {
5029
- cfg.liveMounts = input.liveMounts.map(({ guest, port }) => ({ guest, port }));
4966
+ cfg.liveMounts = input.liveMounts.map((lm) => ({ guest: lm.guest, tag: lm.tag }));
5030
4967
  }
5031
4968
  return cfg;
5032
4969
  }
@@ -5064,7 +5001,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5064
5001
  const synthBundleDir = join11(tempDir, "bundle");
5065
5002
  const cleanup = () => {
5066
5003
  try {
5067
- rmSync8(tempDir, { recursive: true, force: true });
5004
+ rmSync7(tempDir, { recursive: true, force: true });
5068
5005
  } catch {
5069
5006
  }
5070
5007
  };
@@ -5080,7 +5017,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5080
5017
  let imageConfig;
5081
5018
  if (opts.image) {
5082
5019
  baseAbs = resolve4(opts.cwd ?? process.cwd(), opts.image);
5083
- if (!existsSync13(baseAbs)) {
5020
+ if (!existsSync12(baseAbs)) {
5084
5021
  cleanup();
5085
5022
  throw new BootError("BOOT_IMAGE_NOT_FOUND", `image tarball not found: ${baseAbs}`);
5086
5023
  }
@@ -5093,6 +5030,8 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5093
5030
  effectiveCmd = opts.cmd;
5094
5031
  } else if (typeof opts.snapshot === "string") {
5095
5032
  effectiveCmd = ["/sbin/machinen-restore"];
5033
+ } else if (opts._vmstateRestorePath) {
5034
+ effectiveCmd = ["/sbin/machinen-poweroff"];
5096
5035
  } else if (imageConfig?.cmd) {
5097
5036
  effectiveCmd = imageConfig.cmd;
5098
5037
  }
@@ -5127,7 +5066,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5127
5066
  throw err;
5128
5067
  }
5129
5068
  const hostAbs = resolve4(opts.cwd ?? process.cwd(), opts.mount.host);
5130
- if (!existsSync13(hostAbs)) {
5069
+ if (!existsSync12(hostAbs)) {
5131
5070
  cleanup();
5132
5071
  throw new BootError(
5133
5072
  "BOOT_MOUNT_HOST_NOT_FOUND",
@@ -5154,8 +5093,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5154
5093
  // sourced from a snapshot bundle) rides on virtio-blk slots
5155
5094
  // 5+6, attached further down in boot().
5156
5095
  mountGuest: mount?.guest ?? opts._restoreMountDisk?.guest,
5157
- env: effectiveEnv,
5158
- fuseAgentPath: liveMounts.length > 0 ? defaultFuseAgentPath() : void 0
5096
+ env: effectiveEnv
5159
5097
  });
5160
5098
  } else {
5161
5099
  packBundle({
@@ -5163,8 +5101,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5163
5101
  out: cpioPath,
5164
5102
  base: baseAbs,
5165
5103
  mount,
5166
- env: effectiveEnv,
5167
- fuseAgentPath: liveMounts.length > 0 ? defaultFuseAgentPath() : void 0
5104
+ env: effectiveEnv
5168
5105
  });
5169
5106
  }
5170
5107
  packerOpts.onPhase?.("cpio-write", Date.now() - packT0);
@@ -5178,13 +5115,13 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5178
5115
  if (opts._restoreMountDisk) {
5179
5116
  try {
5180
5117
  const r = opts._restoreMountDisk;
5181
- if (!existsSync13(r.lowerPath)) {
5118
+ if (!existsSync12(r.lowerPath)) {
5182
5119
  throw new BootError(
5183
5120
  "BOOT_SNAPSHOT_NOT_FOUND",
5184
5121
  `restore: bundle is missing mount-lower at ${r.lowerPath}`
5185
5122
  );
5186
5123
  }
5187
- if (!existsSync13(r.upperPath)) {
5124
+ if (!existsSync12(r.upperPath)) {
5188
5125
  throw new BootError(
5189
5126
  "BOOT_SNAPSHOT_NOT_FOUND",
5190
5127
  `restore: bundle is missing mount-upper at ${r.upperPath}`
@@ -5231,12 +5168,45 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5231
5168
  }
5232
5169
 
5233
5170
  // src/vm/snapshot.ts
5234
- import { spawn as nodeSpawn3 } from "child_process";
5235
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, readdirSync as readdirSync6, statSync as statSync9, writeFileSync as writeFileSync8 } from "fs";
5171
+ import { spawn as nodeSpawn2 } from "child_process";
5172
+ import {
5173
+ copyFileSync as copyFileSync2,
5174
+ existsSync as existsSync13,
5175
+ mkdirSync as mkdirSync11,
5176
+ readdirSync as readdirSync6,
5177
+ statSync as statSync9,
5178
+ unlinkSync as unlinkSync7,
5179
+ writeFileSync as writeFileSync8
5180
+ } from "fs";
5236
5181
  import { join as join12, resolve as resolve5 } from "path";
5237
- import debugLib11 from "debug";
5238
- var debugSnapshot = debugLib11("machinen:snapshot");
5182
+ import debugLib10 from "debug";
5183
+
5184
+ // src/vm/snapshot-engine.ts
5185
+ var VMSTATE_FILE = "state.vmstate";
5186
+ function resolveSnapshotEngine() {
5187
+ const raw = process.env.MACHINEN_SNAPSHOT_ENGINE;
5188
+ if (raw === void 0 || raw === "") {
5189
+ return "vmstate";
5190
+ }
5191
+ const v = raw.trim().toLowerCase();
5192
+ if (v === "criu" || v === "vmstate") {
5193
+ return v;
5194
+ }
5195
+ throw new Error(
5196
+ `MACHINEN_SNAPSHOT_ENGINE must be "criu" or "vmstate" (got ${JSON.stringify(raw)})`
5197
+ );
5198
+ }
5199
+
5200
+ // src/vm/snapshot.ts
5201
+ var debugSnapshot = debugLib10("machinen:snapshot");
5202
+ var debugVmstate = debugLib10("machinen:vmstate");
5239
5203
  async function performSnapshot(ctx, opts) {
5204
+ if (resolveSnapshotEngine() === "vmstate") {
5205
+ return performSnapshotVmstate(ctx, opts);
5206
+ }
5207
+ return performSnapshotCriu(ctx, opts);
5208
+ }
5209
+ async function performSnapshotCriu(ctx, opts) {
5240
5210
  const baseDumpCmd = opts.dumpCmd ?? "/sbin/machinen-dump";
5241
5211
  const deadlineMs = opts.timeoutMs ?? 9e4;
5242
5212
  const onLog = opts.onLog;
@@ -5278,7 +5248,6 @@ async function performSnapshot(ctx, opts) {
5278
5248
  const imgEntries = readdirSync6(imgDir);
5279
5249
  validateDumpResult(dumpRes, imgEntries, consoleLog, deadlineMs);
5280
5250
  const mountDiskMeta = await reflinkMountOverlay(ctx, snapDir, deadlineMs);
5281
- await manageLiveMountServers(ctx, leaveRunning, deadlineMs, onLog);
5282
5251
  if (!leaveRunning) {
5283
5252
  phases.start("poweroff");
5284
5253
  await powerOffSourceVm(ctx, deadlineMs);
@@ -5286,15 +5255,15 @@ async function performSnapshot(ctx, opts) {
5286
5255
  }
5287
5256
  phases.end("validation");
5288
5257
  phases.start("finalize");
5289
- writeSnapshotMeta(ctx, snapDir, mountDiskMeta);
5258
+ writeSnapshotMeta(ctx, snapDir, mountDiskMeta, "criu");
5290
5259
  phases.end("finalize");
5291
5260
  debugSnapshot("snapshot done snapDir=%s imgEntries=%d", snapDir, imgEntries.length);
5292
5261
  phases.flush(debugSnapshot, "snapshot");
5293
- return { snapDir, imgDir, elapsedMs, consoleLog };
5262
+ return { engine: "criu", snapDir, imgDir, elapsedMs, consoleLog };
5294
5263
  }
5295
- function prepareBundleDir(outDir) {
5264
+ function prepareBundleRootDir(outDir) {
5296
5265
  const snapDir = resolve5(outDir);
5297
- if (existsSync14(snapDir)) {
5266
+ if (existsSync13(snapDir)) {
5298
5267
  if (!statSync9(snapDir).isDirectory()) {
5299
5268
  throw new SnapshotError(
5300
5269
  "SNAPSHOT_DUMP_FAILED",
@@ -5310,12 +5279,16 @@ function prepareBundleDir(outDir) {
5310
5279
  } else {
5311
5280
  mkdirSync11(snapDir, { recursive: true });
5312
5281
  }
5282
+ return snapDir;
5283
+ }
5284
+ function prepareBundleDir(outDir) {
5285
+ const snapDir = prepareBundleRootDir(outDir);
5313
5286
  const imgDir = join12(snapDir, "img");
5314
5287
  mkdirSync11(imgDir, { recursive: true });
5315
5288
  return { snapDir, imgDir };
5316
5289
  }
5317
5290
  async function runDumpAndExtractTar(ctx, dumpCmd, deadlineMs, onLog, imgDir) {
5318
- const tarChild = nodeSpawn3("tar", ["-xmf", "-", "-C", imgDir], {
5291
+ const tarChild = nodeSpawn2("tar", ["-xmf", "-", "-C", imgDir], {
5319
5292
  stdio: ["pipe", "ignore", "pipe"]
5320
5293
  });
5321
5294
  let tarStderr = "";
@@ -5461,48 +5434,6 @@ async function reflinkMountOverlay(ctx, snapDir, deadlineMs) {
5461
5434
  upper: upperName
5462
5435
  };
5463
5436
  }
5464
- async function manageLiveMountServers(ctx, leaveRunning, deadlineMs, onLog) {
5465
- if (!ctx.liveMounts || ctx.liveMounts.length === 0) {
5466
- return;
5467
- }
5468
- if (ctx.stopLiveMountServers) {
5469
- try {
5470
- await ctx.stopLiveMountServers();
5471
- } catch (err) {
5472
- debugSnapshot(
5473
- "stopLiveMountServers failed (continuing): %s",
5474
- err instanceof Error ? err.message : String(err)
5475
- );
5476
- }
5477
- }
5478
- if (!leaveRunning) {
5479
- return;
5480
- }
5481
- if (ctx.respawnLiveMountServers) {
5482
- try {
5483
- await ctx.respawnLiveMountServers();
5484
- } catch (err) {
5485
- throw new SnapshotError(
5486
- "SNAPSHOT_DUMP_FAILED",
5487
- `vm.snapshot: failed to respawn live-mount servers post-dump: ${err instanceof Error ? err.message : String(err)}`,
5488
- { cause: err }
5489
- );
5490
- }
5491
- }
5492
- try {
5493
- await ctx.execRaw("/sbin/machinen-remount", {
5494
- connectTimeoutMs: Math.min(deadlineMs, 5e3),
5495
- execTimeoutMs: 15e3,
5496
- onStderr: onLog ? (chunk) => onLog({ source: "exec-stderr", cmd: "/sbin/machinen-remount", chunk }) : void 0
5497
- });
5498
- } catch (err) {
5499
- throw new SnapshotError(
5500
- "SNAPSHOT_DUMP_FAILED",
5501
- `vm.snapshot: post-dump remount in guest failed: ${err instanceof Error ? err.message : String(err)}`,
5502
- { cause: err }
5503
- );
5504
- }
5505
- }
5506
5437
  async function powerOffSourceVm(ctx, deadlineMs) {
5507
5438
  debugSnapshot("issuing /sbin/machinen-poweroff to bring VMM down");
5508
5439
  ctx.execRaw("/sbin/machinen-poweroff", {
@@ -5525,8 +5456,9 @@ async function powerOffSourceVm(ctx, deadlineMs) {
5525
5456
  clearTimeout(powerOffTimer);
5526
5457
  }
5527
5458
  }
5528
- function writeSnapshotMeta(ctx, snapDir, mountDiskMeta) {
5459
+ function writeSnapshotMeta(ctx, snapDir, mountDiskMeta, engine) {
5529
5460
  const meta = {
5461
+ engine,
5530
5462
  sourceName: ctx.sourceName,
5531
5463
  sourceImage: ctx.sourceImage,
5532
5464
  snappedAt: Date.now(),
@@ -5535,7 +5467,11 @@ function writeSnapshotMeta(ctx, snapDir, mountDiskMeta) {
5535
5467
  // snapshotting host that the restoring host has to resolve (or
5536
5468
  // remap via `restore({ liveMounts })`). Recorded only when ctx
5537
5469
  // carried a resolved list (boot-handle path).
5538
- liveMounts: ctx.liveMounts && ctx.liveMounts.length > 0 ? ctx.liveMounts.map(({ guest, host, mode }) => ({ guest, host, mode })) : void 0
5470
+ liveMounts: ctx.liveMounts && ctx.liveMounts.length > 0 ? ctx.liveMounts.map(({ guest, host, mode }) => ({
5471
+ guest,
5472
+ host,
5473
+ mode
5474
+ })) : void 0
5539
5475
  };
5540
5476
  writeFileSync8(join12(snapDir, "meta.json"), JSON.stringify(meta));
5541
5477
  }
@@ -5555,14 +5491,107 @@ Dump exec exited ${outcome.exitCode} (workload kept running).`;
5555
5491
  }
5556
5492
  return "";
5557
5493
  }
5494
+ async function performSnapshotVmstate(ctx, opts) {
5495
+ const t0 = Date.now();
5496
+ const deadlineMs = opts.timeoutMs ?? 9e4;
5497
+ const leaveRunning = opts.leaveRunning === true;
5498
+ if (!ctx.vmstatePath) {
5499
+ throw new SnapshotError(
5500
+ "SNAPSHOT_NO_DISK",
5501
+ "vm.snapshot: this VM was not booted with the vmstate engine.\n Set MACHINEN_SNAPSHOT_ENGINE=vmstate before `machinen boot` so the\n VMM installs the SIGUSR1 whole-VM dump handler."
5502
+ );
5503
+ }
5504
+ const snapDir = prepareBundleRootDir(opts.outDir);
5505
+ debugVmstate(
5506
+ "vmstate snapshot start pid=%d snapDir=%s vmstatePath=%s leaveRunning=%s",
5507
+ ctx.pid,
5508
+ snapDir,
5509
+ ctx.vmstatePath,
5510
+ leaveRunning
5511
+ );
5512
+ try {
5513
+ unlinkSync7(ctx.vmstatePath);
5514
+ } catch {
5515
+ }
5516
+ try {
5517
+ await ctx.execRaw("sync; sync /mnt 2>/dev/null; true", {
5518
+ connectTimeoutMs: Math.min(deadlineMs, 5e3),
5519
+ execTimeoutMs: 1e4
5520
+ });
5521
+ } catch (err) {
5522
+ debugVmstate(
5523
+ "pre-snapshot sync failed (continuing): %s",
5524
+ err instanceof Error ? err.message : String(err)
5525
+ );
5526
+ }
5527
+ try {
5528
+ process.kill(ctx.pid, "SIGUSR1");
5529
+ } catch (err) {
5530
+ throw new SnapshotError(
5531
+ "SNAPSHOT_DUMP_FAILED",
5532
+ `vm.snapshot: failed to signal the VMM (pid ${ctx.pid}): ${err instanceof Error ? err.message : String(err)}`,
5533
+ { cause: err }
5534
+ );
5535
+ }
5536
+ await waitForVmstateFile(ctx.vmstatePath, deadlineMs);
5537
+ const bundleStatePath = join12(snapDir, VMSTATE_FILE);
5538
+ try {
5539
+ copyFileSync2(ctx.vmstatePath, bundleStatePath);
5540
+ } catch (err) {
5541
+ throw new SnapshotError(
5542
+ "SNAPSHOT_DUMP_FAILED",
5543
+ `vm.snapshot: failed to copy the .vmstate into the bundle: ${err instanceof Error ? err.message : String(err)}`,
5544
+ { cause: err }
5545
+ );
5546
+ }
5547
+ const mountDiskMeta = await reflinkMountOverlay(ctx, snapDir, deadlineMs);
5548
+ writeSnapshotMeta(ctx, snapDir, mountDiskMeta, "vmstate");
5549
+ if (!leaveRunning) {
5550
+ await powerOffSourceVm(ctx, deadlineMs);
5551
+ }
5552
+ const elapsedMs = Date.now() - t0;
5553
+ const stateBytes = statSync9(bundleStatePath).size;
5554
+ debugVmstate(
5555
+ "vmstate snapshot done snapDir=%s stateBytes=%d elapsedMs=%d",
5556
+ snapDir,
5557
+ stateBytes,
5558
+ elapsedMs
5559
+ );
5560
+ return {
5561
+ engine: "vmstate",
5562
+ snapDir,
5563
+ vmstatePath: bundleStatePath,
5564
+ elapsedMs,
5565
+ consoleLog: await ctx.errorOutput()
5566
+ };
5567
+ }
5568
+ async function waitForVmstateFile(path, deadlineMs) {
5569
+ const deadline = Date.now() + deadlineMs;
5570
+ while (Date.now() < deadline) {
5571
+ try {
5572
+ if (statSync9(path).size > 0) {
5573
+ return;
5574
+ }
5575
+ } catch {
5576
+ }
5577
+ await new Promise((r) => setTimeout(r, 100));
5578
+ }
5579
+ throw new SnapshotError(
5580
+ "SNAPSHOT_TIMEOUT",
5581
+ `vm.snapshot: the VMM did not write its .vmstate within ${deadlineMs}ms (${path}).
5582
+ The VM may not have been booted with MACHINEN_SNAPSHOT_ENGINE=vmstate.`
5583
+ );
5584
+ }
5558
5585
 
5559
5586
  // src/vm/boot.ts
5560
- var debug11 = debugLib12("machinen:boot");
5561
- var vmmDebug = debugLib12("machinen:vmm");
5587
+ var debug10 = debugLib11("machinen:boot");
5588
+ var vmmDebug = debugLib11("machinen:vmm");
5589
+ var vmstateDebug = debugLib11("machinen:vmstate");
5590
+ var restoreDebug = debugLib11("machinen:restore");
5562
5591
  async function boot(opts = {}) {
5563
5592
  const bootT0 = Date.now();
5564
5593
  const phases = new PhaseTimer();
5565
- debug11(
5594
+ debug10(
5566
5595
  "boot entry image=%s cmd=%j name=%s portForward=%d hasSnapshot=%s mount=%s",
5567
5596
  opts.image ?? "<none>",
5568
5597
  opts.cmd ?? null,
@@ -5576,7 +5605,7 @@ async function boot(opts = {}) {
5576
5605
  await validatePortForwardOpts(opts, portForward);
5577
5606
  const binaryInput = opts.binary ?? resolveVmmBinary();
5578
5607
  const binary = resolve6(opts.cwd ?? process.cwd(), binaryInput);
5579
- if (!existsSync15(binary)) {
5608
+ if (!existsSync14(binary)) {
5580
5609
  throw new BootError("BOOT_VMM_MISSING", `VMM binary not found at ${binary}`);
5581
5610
  }
5582
5611
  if (opts.cmd && !opts.image) {
@@ -5601,21 +5630,31 @@ async function boot(opts = {}) {
5601
5630
  validateKernelDtb(opts, env);
5602
5631
  let { vsockUdsPath, vsockTempDir } = setupVsockBridge(env);
5603
5632
  const { statsFilePath, statsTempDir } = setupStatsFile(env, vsockTempDir);
5604
- let liveMountsResolved = [];
5605
- if ((opts.liveMounts ?? []).length > 0) {
5633
+ let vmstateStatePath;
5634
+ if (resolveSnapshotEngine() === "vmstate" && opts.snapshot !== false) {
5606
5635
  if (!vsockTempDir) {
5607
5636
  vsockTempDir = mkdtempSync6(join13(tmpdir5(), "machinen-vsock-"));
5608
5637
  }
5609
- liveMountsResolved = resolveLiveMounts(opts.liveMounts, opts.cwd, vsockTempDir);
5610
- for (const lm of liveMountsResolved) {
5611
- env.MACHINEN_VSOCK = `${env.MACHINEN_VSOCK},out:${lm.port}:${lm.udsPath}`;
5638
+ vmstateStatePath = join13(vsockTempDir, VMSTATE_FILE);
5639
+ env.MACHINEN_SNAPSHOT_PATH = vmstateStatePath;
5640
+ }
5641
+ if (opts._vmstateRestorePath) {
5642
+ env.MACHINEN_RESTORE_PATH = opts._vmstateRestorePath;
5643
+ if ((vmstateDebug.enabled || restoreDebug.enabled) && !env.MACHINEN_VMSTATE_TIMING) {
5644
+ env.MACHINEN_VMSTATE_TIMING = "1";
5612
5645
  }
5613
5646
  }
5647
+ let liveMountsResolved = [];
5648
+ if ((opts.liveMounts ?? []).length > 0) {
5649
+ liveMountsResolved = resolveLiveMounts(opts.liveMounts, opts.cwd);
5650
+ liveMountsResolved.forEach((lm, i) => {
5651
+ env[`MACHINEN_VIRTIOFS_${i}`] = `${lm.tag}:${lm.mode}:${lm.host}`;
5652
+ });
5653
+ }
5614
5654
  let gvStop;
5615
5655
  let gvPid;
5616
5656
  let gvExe;
5617
5657
  let gvSocketDir;
5618
- const liveMountServers = [];
5619
5658
  let bundleTempDir;
5620
5659
  let mountDiskPaths;
5621
5660
  let perBootMountUpper;
@@ -5624,6 +5663,9 @@ async function boot(opts = {}) {
5624
5663
  if (opts.name && !mergedGuestEnv.MACHINEN_VM_NAME) {
5625
5664
  mergedGuestEnv.MACHINEN_VM_NAME = opts.name;
5626
5665
  }
5666
+ if (vsockUdsPath && !mergedGuestEnv.MACHINEN_VM_HOSTNAME_WAIT) {
5667
+ mergedGuestEnv.MACHINEN_VM_HOSTNAME_WAIT = "1";
5668
+ }
5627
5669
  try {
5628
5670
  phases.start("net-services");
5629
5671
  phases.start("net-services.gvproxy");
@@ -5646,7 +5688,7 @@ async function boot(opts = {}) {
5646
5688
  env.MACHINEN_INITRD = packed.cpioPath;
5647
5689
  mountDiskPaths = packed.mountDisk;
5648
5690
  const packMs = phases.end("initramfs-pack");
5649
- debug11("initramfs packed cpio=%s elapsed=%dms", packed.cpioPath, packMs ?? -1);
5691
+ debug10("initramfs packed cpio=%s elapsed=%dms", packed.cpioPath, packMs ?? -1);
5650
5692
  }
5651
5693
  phases.start("rootdisk-materialize");
5652
5694
  if (wantsRootDisk) {
@@ -5672,7 +5714,7 @@ async function boot(opts = {}) {
5672
5714
  if (mountDiskPaths) {
5673
5715
  perBootMountUpper = mountDiskPaths.upperPath;
5674
5716
  }
5675
- const child = nodeSpawn4(wrappedVmm.command, wrappedVmm.args, {
5717
+ const child = nodeSpawn3(wrappedVmm.command, wrappedVmm.args, {
5676
5718
  cwd: opts.cwd,
5677
5719
  env,
5678
5720
  stdio
@@ -5682,7 +5724,7 @@ async function boot(opts = {}) {
5682
5724
  }
5683
5725
  phases.end("vmm-spawn");
5684
5726
  phases.start("first-guest-byte");
5685
- debug11(
5727
+ debug10(
5686
5728
  "VMM spawned pid=%d binary=%s wrapped=%s elapsedSinceEntry=%dms",
5687
5729
  child.pid ?? -1,
5688
5730
  binary,
@@ -5722,7 +5764,8 @@ async function boot(opts = {}) {
5722
5764
  memoryCeilingMib,
5723
5765
  statsFilePath,
5724
5766
  mountDiskPaths,
5725
- liveMountsResolved
5767
+ liveMountsResolved,
5768
+ vmstateStatePath
5726
5769
  }) : false;
5727
5770
  installVmExitCleanup({
5728
5771
  child,
@@ -5735,7 +5778,6 @@ async function boot(opts = {}) {
5735
5778
  vsockTempDir,
5736
5779
  statsTempDir,
5737
5780
  gvStop,
5738
- liveMountServers,
5739
5781
  registered
5740
5782
  });
5741
5783
  const timeoutMs = opts.timeoutMs === void 0 ? 6e4 : opts.timeoutMs;
@@ -5752,22 +5794,12 @@ async function boot(opts = {}) {
5752
5794
  process.stderr.write(chunk);
5753
5795
  });
5754
5796
  }
5797
+ installVmstateTimingRelay(child);
5755
5798
  installFlushPhases(child, phases, onLog);
5756
5799
  const detachedBootChunks = [];
5757
5800
  if (opts.detached) {
5758
5801
  installDetachedBootCapture(child, detachedBootChunks);
5759
5802
  }
5760
- if (liveMountsResolved.length > 0) {
5761
- phases.start("net-services.live-mounts");
5762
- await spawnLiveMountServersForBoot({
5763
- liveMountsResolved,
5764
- childPid,
5765
- child,
5766
- registered,
5767
- liveMountServers
5768
- });
5769
- phases.end("net-services.live-mounts");
5770
- }
5771
5803
  const handle = {
5772
5804
  pid: childPid,
5773
5805
  name: vmName,
@@ -5836,20 +5868,16 @@ ${res.stderr}`
5836
5868
  const balloon = statsFilePath ? readBalloonStats(statsFilePath) : null;
5837
5869
  const cur = findEntry({ pid: childPid });
5838
5870
  const lazyTotal = cur?.lazyPagesTotal ?? 0;
5839
- let bytesServed = 0;
5840
- for (const server of liveMountServers) {
5841
- bytesServed += server.bytesServedOnPagesImg();
5842
- }
5843
- const pagesServed = Math.floor(bytesServed / 4096);
5844
5871
  return {
5845
5872
  ceilingMib: memoryCeilingMib ?? null,
5846
5873
  hostRssBytes: readHostRssBytes(childPid, statsFilePath),
5847
5874
  balloonInflatedBytes: balloon?.bytesReported ?? 0,
5848
- lazyPagesPending: Math.max(0, lazyTotal - pagesServed)
5875
+ lazyPagesPending: lazyTotal
5849
5876
  };
5850
5877
  },
5851
5878
  async snapshot(snapshotOpts) {
5852
- if (!diskAbs) {
5879
+ const engine = resolveSnapshotEngine();
5880
+ if (engine === "criu" && !diskAbs || engine === "vmstate" && !vmstateStatePath) {
5853
5881
  throw new SnapshotError(
5854
5882
  "SNAPSHOT_NO_DISK",
5855
5883
  "vm.snapshot: this VM was booted with `snapshot: false` (no scratch disk attached). Re-boot without that flag \u2014 the runtime will auto-allocate a sparse scratch \u2014 or pass `snapshot: '<path>'`."
@@ -5858,7 +5886,8 @@ ${res.stderr}`
5858
5886
  return performSnapshot(buildBootSnapshotContext(), snapshotOpts);
5859
5887
  },
5860
5888
  async fork(forkOpts) {
5861
- if (!diskAbs) {
5889
+ const engine = resolveSnapshotEngine();
5890
+ if (engine === "criu" && !diskAbs || engine === "vmstate" && !vmstateStatePath) {
5862
5891
  throw new SnapshotError(
5863
5892
  "SNAPSHOT_NO_DISK",
5864
5893
  "vm.fork: source VM has no scratch disk (booted with `snapshot: false`). Re-boot the source without that flag so it can be snapshotted."
@@ -5884,23 +5913,7 @@ ${res.stderr}`
5884
5913
  upperPath: mountDiskPaths.upperPath
5885
5914
  } : void 0,
5886
5915
  liveMounts: liveMountsForCtx,
5887
- stopLiveMountServers: liveMountsForCtx ? async () => {
5888
- await Promise.all(liveMountServers.map((s) => s.stop().catch(() => {
5889
- })));
5890
- liveMountServers.length = 0;
5891
- } : void 0,
5892
- respawnLiveMountServers: liveMountsForCtx ? async () => {
5893
- for (const lm of liveMountsResolved) {
5894
- const fresh = await spawnDetachedMountServer({
5895
- udsPath: lm.udsPath,
5896
- rootAbs: lm.host,
5897
- mode: lm.mode,
5898
- vmmPid: childPid,
5899
- statsPath: lm.statsPath
5900
- });
5901
- liveMountServers.push(fresh);
5902
- }
5903
- } : void 0,
5916
+ vmstatePath: vmstateStatePath,
5904
5917
  execRaw: (cmd, execOpts) => handle.execRaw(cmd, execOpts),
5905
5918
  wait: () => handle.wait(),
5906
5919
  kill: () => handle.kill(),
@@ -5984,12 +5997,12 @@ function prepareScratchDisk(opts, env) {
5984
5997
  }
5985
5998
  if (typeof opts.snapshot === "string") {
5986
5999
  const bundleDisk = resolve6(opts.cwd ?? process.cwd(), opts.snapshot);
5987
- if (!existsSync15(bundleDisk)) {
6000
+ if (!existsSync14(bundleDisk)) {
5988
6001
  throw new BootError("BOOT_SNAPSHOT_NOT_FOUND", `snapshot image not found: ${bundleDisk}`);
5989
6002
  }
5990
6003
  if (opts.cmd) {
5991
6004
  env.MACHINEN_DISK = bundleDisk;
5992
- debug11("snap-restore in-place (explicit cmd) path=%s", bundleDisk);
6005
+ debug10("snap-restore in-place (explicit cmd) path=%s", bundleDisk);
5993
6006
  return { diskAbs: bundleDisk, perBootSnapDisk: void 0 };
5994
6007
  }
5995
6008
  const perBoot = join13(
@@ -5998,7 +6011,7 @@ function prepareScratchDisk(opts, env) {
5998
6011
  );
5999
6012
  reflinkCopy(bundleDisk, perBoot);
6000
6013
  env.MACHINEN_DISK = perBoot;
6001
- debug11("snap-restore reflink-clone src=%s dst=%s", bundleDisk, perBoot);
6014
+ debug10("snap-restore reflink-clone src=%s dst=%s", bundleDisk, perBoot);
6002
6015
  return { diskAbs: perBoot, perBootSnapDisk: perBoot };
6003
6016
  }
6004
6017
  if (!opts.image) {
@@ -6010,20 +6023,20 @@ function prepareScratchDisk(opts, env) {
6010
6023
  );
6011
6024
  allocateSparseFile3(scratchPath, SNAP_SCRATCH_BYTES);
6012
6025
  env.MACHINEN_DISK = scratchPath;
6013
- debug11("snap-scratch auto path=%s sizeBytes=%d", scratchPath, SNAP_SCRATCH_BYTES);
6026
+ debug10("snap-scratch auto path=%s sizeBytes=%d", scratchPath, SNAP_SCRATCH_BYTES);
6014
6027
  return { diskAbs: scratchPath, perBootSnapDisk: scratchPath };
6015
6028
  }
6016
6029
  function validateKernelDtb(opts, env) {
6017
6030
  if (opts.kernel) {
6018
6031
  const abs = resolve6(opts.cwd ?? process.cwd(), opts.kernel);
6019
- if (!existsSync15(abs)) {
6032
+ if (!existsSync14(abs)) {
6020
6033
  throw new BootError("BOOT_KERNEL_NOT_FOUND", `kernel not found: ${abs}`);
6021
6034
  }
6022
6035
  env.MACHINEN_KERNEL = abs;
6023
6036
  }
6024
6037
  if (opts.dtb) {
6025
6038
  const abs = resolve6(opts.cwd ?? process.cwd(), opts.dtb);
6026
- if (!existsSync15(abs)) {
6039
+ if (!existsSync14(abs)) {
6027
6040
  throw new BootError("BOOT_DTB_NOT_FOUND", `dtb not found: ${abs}`);
6028
6041
  }
6029
6042
  env.MACHINEN_DTB = abs;
@@ -6032,7 +6045,7 @@ function validateKernelDtb(opts, env) {
6032
6045
  function setupVsockBridge(env) {
6033
6046
  if (env.MACHINEN_VSOCK) {
6034
6047
  const vsockUdsPath2 = parseVsockUdsPath(env.MACHINEN_VSOCK);
6035
- debug11(
6048
+ debug10(
6036
6049
  "vsock spec from caller env: %s (uds=%s)",
6037
6050
  env.MACHINEN_VSOCK,
6038
6051
  vsockUdsPath2 ?? "<unparsed>"
@@ -6042,7 +6055,7 @@ function setupVsockBridge(env) {
6042
6055
  const vsockTempDir = mkdtempSync6(join13(tmpdir5(), "machinen-vsock-"));
6043
6056
  const vsockUdsPath = join13(vsockTempDir, "exec.sock");
6044
6057
  env.MACHINEN_VSOCK = `in:1978:${vsockUdsPath}`;
6045
- debug11("vsock auto uds=%s", vsockUdsPath);
6058
+ debug10("vsock auto uds=%s", vsockUdsPath);
6046
6059
  return { vsockUdsPath, vsockTempDir };
6047
6060
  }
6048
6061
  function setupStatsFile(env, vsockTempDir) {
@@ -6068,7 +6081,7 @@ function setupStatsFile(env, vsockTempDir) {
6068
6081
  }
6069
6082
  async function bringUpGvproxy(opts, binary, env, portForward) {
6070
6083
  if (env.MACHINEN_NET_SOCKET) {
6071
- debug11("MACHINEN_NET_SOCKET already set \u2014 skipping gvproxy spawn");
6084
+ debug10("MACHINEN_NET_SOCKET already set \u2014 skipping gvproxy spawn");
6072
6085
  return { gvStop: void 0, gvPid: void 0, gvExe: void 0, gvSocketDir: void 0 };
6073
6086
  }
6074
6087
  const gvBin = await ensureGvproxy(binary);
@@ -6079,11 +6092,11 @@ async function bringUpGvproxy(opts, binary, env, portForward) {
6079
6092
  "portForward requires gvproxy, but no gvproxy binary was found. Install gvproxy or point MACHINEN_GVPROXY at one."
6080
6093
  );
6081
6094
  }
6082
- debug11("gvproxy not found \u2014 booting without networking");
6095
+ debug10("gvproxy not found \u2014 booting without networking");
6083
6096
  warnGvproxyMissing();
6084
6097
  return { gvStop: void 0, gvPid: void 0, gvExe: void 0, gvSocketDir: void 0 };
6085
6098
  }
6086
- debug11("starting gvproxy bin=%s", gvBin);
6099
+ debug10("starting gvproxy bin=%s", gvBin);
6087
6100
  const gv = await spawnGvproxy(gvBin, { detached: opts.detached });
6088
6101
  env.MACHINEN_NET_SOCKET = gv.socketPath;
6089
6102
  for (const m of portForward) {
@@ -6099,7 +6112,7 @@ async function bringUpGvproxy(opts, binary, env, portForward) {
6099
6112
  function materializeRootdisk(opts, env, phases) {
6100
6113
  if (typeof opts.rootDisk === "string") {
6101
6114
  const rootDiskAbs = resolve6(opts.cwd ?? process.cwd(), opts.rootDisk);
6102
- if (!existsSync15(rootDiskAbs)) {
6115
+ if (!existsSync14(rootDiskAbs)) {
6103
6116
  throw new BootError("BOOT_IMAGE_NOT_FOUND", `rootDisk image not found: ${rootDiskAbs}`);
6104
6117
  }
6105
6118
  env.MACHINEN_ROOTDISK = rootDiskAbs;
@@ -6131,7 +6144,7 @@ function rollbackPreSpawn(state) {
6131
6144
  for (const dir of [state.bundleTempDir, state.vsockTempDir]) {
6132
6145
  if (dir) {
6133
6146
  try {
6134
- rmSync9(dir, { recursive: true, force: true });
6147
+ rmSync8(dir, { recursive: true, force: true });
6135
6148
  } catch {
6136
6149
  }
6137
6150
  }
@@ -6139,7 +6152,7 @@ function rollbackPreSpawn(state) {
6139
6152
  for (const file of [state.perBootRootDisk, state.perBootSnapDisk, state.perBootMountUpper]) {
6140
6153
  if (file) {
6141
6154
  try {
6142
- unlinkSync7(file);
6155
+ unlinkSync8(file);
6143
6156
  } catch {
6144
6157
  }
6145
6158
  }
@@ -6249,6 +6262,10 @@ function registerInRegistry(args) {
6249
6262
  portForward: args.portForward.length > 0 ? args.portForward : void 0,
6250
6263
  memoryCeilingMib: args.memoryCeilingMib,
6251
6264
  statsPath: args.statsFilePath,
6265
+ // Vmstate engine: persist the VMM's whole-VM state-file path so
6266
+ // an attach-owned `vm.snapshot()` / `vm.fork()` can SIGUSR1 the
6267
+ // VMM and pick the .vmstate up. Undefined for criu-engine VMs.
6268
+ vmstatePath: args.vmstateStatePath,
6252
6269
  // #272: persist mount-overlay paths so an attach-owned
6253
6270
  // vm.snapshot()/fork() can reflink the lower+upper into the
6254
6271
  // bundle. Without this, `machinen snapshot <vm>` from
@@ -6259,19 +6276,20 @@ function registerInRegistry(args) {
6259
6276
  upperPath: args.mountDiskPaths.upperPath
6260
6277
  } : void 0,
6261
6278
  // #273: persist live-share mount config so an attach-owned
6262
- // snapshot/fork can write the same `meta.liveMounts` block
6263
- // and trigger /sbin/machinen-remount post-dump on
6264
- // leaveRunning paths. Host UDS / vsock port aren't carried —
6265
- // those are this process's private state and the attach side
6266
- // doesn't need them (the source's servers stay listening
6267
- // through the dump and the re-fork'd fuse-agent reconnects).
6268
- liveMounts: args.liveMountsResolved.length > 0 ? args.liveMountsResolved.map(({ guest, host, mode }) => ({ guest, host, mode })) : void 0,
6279
+ // snapshot/fork can write the same `meta.liveMounts` block. The
6280
+ // per-mount virtio-fs tag isn't carried — it's re-derived from
6281
+ // the resolved order on restore.
6282
+ liveMounts: args.liveMountsResolved.length > 0 ? args.liveMountsResolved.map(({ guest, host, mode }) => ({
6283
+ guest,
6284
+ host,
6285
+ mode
6286
+ })) : void 0,
6269
6287
  startedAt: Date.now()
6270
6288
  });
6271
- debug11("registered pid=%d name=%s", args.childPid, args.vmName ?? "<unset>");
6289
+ debug10("registered pid=%d name=%s", args.childPid, args.vmName ?? "<unset>");
6272
6290
  return true;
6273
6291
  } catch (err) {
6274
- debug11(
6292
+ debug10(
6275
6293
  "registry write failed (best-effort) err=%s",
6276
6294
  err instanceof Error ? err.message : String(err)
6277
6295
  );
@@ -6280,7 +6298,7 @@ function registerInRegistry(args) {
6280
6298
  }
6281
6299
  function installVmExitCleanup(state) {
6282
6300
  state.child.once("exit", (code, signal) => {
6283
- debug11(
6301
+ debug10(
6284
6302
  "VMM exit pid=%d code=%s signal=%s lifetimeMs=%d",
6285
6303
  state.childPid,
6286
6304
  code,
@@ -6290,7 +6308,7 @@ function installVmExitCleanup(state) {
6290
6308
  for (const file of [state.perBootRootDisk, state.perBootSnapDisk, state.perBootMountUpper]) {
6291
6309
  if (file) {
6292
6310
  try {
6293
- unlinkSync7(file);
6311
+ unlinkSync8(file);
6294
6312
  } catch {
6295
6313
  }
6296
6314
  }
@@ -6298,7 +6316,7 @@ function installVmExitCleanup(state) {
6298
6316
  for (const dir of [state.bundleTempDir, state.vsockTempDir, state.statsTempDir]) {
6299
6317
  if (dir) {
6300
6318
  try {
6301
- rmSync9(dir, { recursive: true, force: true });
6319
+ rmSync8(dir, { recursive: true, force: true });
6302
6320
  } catch {
6303
6321
  }
6304
6322
  }
@@ -6306,15 +6324,37 @@ function installVmExitCleanup(state) {
6306
6324
  if (state.gvStop) {
6307
6325
  state.gvStop();
6308
6326
  }
6309
- for (const server of state.liveMountServers) {
6310
- void server.stop().catch(() => {
6311
- });
6312
- }
6313
6327
  if (state.registered) {
6314
6328
  removeEntry(state.childPid);
6315
6329
  }
6316
6330
  });
6317
6331
  }
6332
+ function installVmstateTimingRelay(child) {
6333
+ if (!vmstateDebug.enabled && !restoreDebug.enabled) {
6334
+ return;
6335
+ }
6336
+ const timingDebug = vmstateDebug.enabled ? vmstateDebug : restoreDebug;
6337
+ let carry = "";
6338
+ const flushLine = (line) => {
6339
+ if (line.startsWith("vmstate restore timing ")) {
6340
+ timingDebug("%s", line);
6341
+ }
6342
+ };
6343
+ child.stderr.on("data", (chunk) => {
6344
+ const text = carry + chunk.toString("utf8");
6345
+ const lines = text.split("\n");
6346
+ carry = lines.pop() ?? "";
6347
+ for (const line of lines) {
6348
+ flushLine(line);
6349
+ }
6350
+ });
6351
+ child.once("exit", () => {
6352
+ if (carry) {
6353
+ flushLine(carry);
6354
+ carry = "";
6355
+ }
6356
+ });
6357
+ }
6318
6358
  function installFlushPhases(child, phases, onLog) {
6319
6359
  let phasesFlushed = false;
6320
6360
  const flush = () => {
@@ -6323,7 +6363,7 @@ function installFlushPhases(child, phases, onLog) {
6323
6363
  }
6324
6364
  phasesFlushed = true;
6325
6365
  phases.end("first-guest-byte");
6326
- phases.flush(debug11, "boot");
6366
+ phases.flush(debug10, "boot");
6327
6367
  onLog?.(phases.toEvent("boot"));
6328
6368
  };
6329
6369
  child.stderr.once("data", flush);
@@ -6339,39 +6379,6 @@ function installDetachedBootCapture(child, sink) {
6339
6379
  }
6340
6380
  });
6341
6381
  }
6342
- async function spawnLiveMountServersForBoot(args) {
6343
- try {
6344
- for (const lm of args.liveMountsResolved) {
6345
- const lmHandle = await spawnDetachedMountServer({
6346
- udsPath: lm.udsPath,
6347
- rootAbs: lm.host,
6348
- mode: lm.mode,
6349
- vmmPid: args.childPid,
6350
- statsPath: lm.statsPath
6351
- });
6352
- args.liveMountServers.push(lmHandle);
6353
- }
6354
- } catch (err) {
6355
- try {
6356
- args.child.kill("SIGKILL");
6357
- } catch {
6358
- }
6359
- throw err;
6360
- }
6361
- if (!args.registered) {
6362
- return;
6363
- }
6364
- try {
6365
- patchEntry(args.childPid, {
6366
- liveMountServers: args.liveMountServers.map((h) => ({ pid: h.pid, exe: h.exe }))
6367
- });
6368
- } catch (err) {
6369
- debug11(
6370
- "registry patch (liveMountServers) failed (best-effort) err=%s",
6371
- err instanceof Error ? err.message : String(err)
6372
- );
6373
- }
6374
- }
6375
6382
  async function gateOnDetachedReadiness(args) {
6376
6383
  const readinessTimeoutMs = args.timeoutMs ?? 6e4;
6377
6384
  let onByte = null;
@@ -6456,14 +6463,17 @@ function makeKill(child) {
6456
6463
  }
6457
6464
 
6458
6465
  // src/vm/restore.ts
6459
- var debug12 = debugLib13("machinen:boot");
6460
- var debugRestore = debugLib13("machinen:restore");
6466
+ var debug11 = debugLib12("machinen:boot");
6467
+ var debugRestore = debugLib12("machinen:restore");
6461
6468
  async function restore(opts) {
6462
6469
  const phases = new PhaseTimer();
6463
6470
  const snapDir = resolve7(opts.snapDir);
6471
+ if (existsSync15(join14(snapDir, VMSTATE_FILE))) {
6472
+ return restoreVmstate(opts, snapDir);
6473
+ }
6464
6474
  const imgDir = join14(snapDir, "img");
6465
6475
  const metaPath = join14(snapDir, "meta.json");
6466
- if (!existsSync16(imgDir) || !statSync10(imgDir).isDirectory()) {
6476
+ if (!existsSync15(imgDir) || !statSync10(imgDir).isDirectory()) {
6467
6477
  throw new BootError("BOOT_SNAPSHOT_NOT_FOUND", `restore: ${imgDir} not found`);
6468
6478
  }
6469
6479
  const imgEntries = readdirSync7(imgDir);
@@ -6475,50 +6485,21 @@ async function restore(opts) {
6475
6485
  }
6476
6486
  phases.start("snapshot-meta-read");
6477
6487
  let meta = { snappedAt: 0 };
6478
- if (existsSync16(metaPath)) {
6488
+ if (existsSync15(metaPath)) {
6479
6489
  try {
6480
- meta = JSON.parse(readFileSync10(metaPath, "utf8"));
6490
+ meta = JSON.parse(readFileSync9(metaPath, "utf8"));
6481
6491
  } catch {
6482
6492
  }
6483
6493
  }
6484
6494
  phases.end("snapshot-meta-read");
6485
- let resolvedImage;
6486
- if (opts.image) {
6487
- resolvedImage = resolve7(opts.cwd ?? process.cwd(), opts.image);
6488
- if (!existsSync16(resolvedImage)) {
6489
- throw new BootError("BOOT_IMAGE_NOT_FOUND", `restore: image not found: ${resolvedImage}`);
6490
- }
6491
- } else if (meta.sourceImage && existsSync16(meta.sourceImage)) {
6492
- resolvedImage = meta.sourceImage;
6493
- debugRestore("using meta.sourceImage path=%s", resolvedImage);
6494
- } else if (meta.sourceImage) {
6495
- throw new BootError(
6496
- "BOOT_IMAGE_NOT_FOUND",
6497
- `restore: source image not found at ${meta.sourceImage}
6498
- The snapshot was taken with this rootfs tarball, and CRIU needs
6499
- it to reopen the process's file-backed memory mappings (e.g.
6500
- /usr/bin/node, libc, etc).
6501
- \u2022 copy the tarball to that path on this host, OR
6502
- \u2022 pass an explicit override via the runtime's restore({ image })
6503
- or the CLI's \`machinen restore --image <tarball>\`.`
6504
- );
6505
- } else {
6506
- throw new BootError(
6507
- "BOOT_IMAGE_NOT_FOUND",
6508
- `restore: no rootfs image available for this bundle.
6509
- The snapshot's meta.json doesn't record a source image (likely
6510
- predates the field). Pass the same tarball you booted the
6511
- source VM with via the runtime's restore({ image }) or the
6512
- CLI's \`machinen restore --image <tarball>\`.`
6513
- );
6514
- }
6495
+ const resolvedImage = resolveRestoreImage(opts, meta);
6515
6496
  const lazyPages = opts.lazy === true;
6516
6497
  let lazyPagesTotal;
6517
6498
  if (lazyPages) {
6518
6499
  phases.start("snapshot-mark-lazy");
6519
6500
  const marked = markPagemapsLazy(imgDir);
6520
6501
  lazyPagesTotal = marked.entriesFlagged + marked.entriesAlreadyLazy;
6521
- debug12(
6502
+ debug11(
6522
6503
  "lazy-pages mark: files=%d entriesFlagged=%d alreadyLazy=%d",
6523
6504
  marked.filesRewritten,
6524
6505
  marked.entriesFlagged,
@@ -6559,7 +6540,7 @@ async function restore(opts) {
6559
6540
  }
6560
6541
  } catch (err) {
6561
6542
  try {
6562
- unlinkSync8(scratchPath);
6543
+ unlinkSync9(scratchPath);
6563
6544
  } catch {
6564
6545
  }
6565
6546
  throw new BootError(
@@ -6574,7 +6555,7 @@ async function restore(opts) {
6574
6555
  if (meta.mountDisk) {
6575
6556
  const lowerAbs = join14(snapDir, meta.mountDisk.lower);
6576
6557
  const upperAbs = join14(snapDir, meta.mountDisk.upper);
6577
- if (!existsSync16(lowerAbs) || !existsSync16(upperAbs)) {
6558
+ if (!existsSync15(lowerAbs) || !existsSync15(upperAbs)) {
6578
6559
  throw new BootError(
6579
6560
  "BOOT_SNAPSHOT_NOT_FOUND",
6580
6561
  `restore: bundle's mount overlay is missing one of:
@@ -6603,31 +6584,18 @@ async function restore(opts) {
6603
6584
  });
6604
6585
  } finally {
6605
6586
  try {
6606
- unlinkSync8(scratchPath);
6587
+ unlinkSync9(scratchPath);
6607
6588
  } catch {
6608
6589
  }
6609
6590
  }
6610
6591
  phases.end("boot");
6611
- if (!opts.name && meta.sourceName) {
6612
- const candidates = [`${meta.sourceName}/${vm.pid}`, `${meta.sourceName}~${vm.pid}`];
6613
- for (const candidate of candidates) {
6614
- if (claimName(candidate, vm.pid)) {
6615
- const cur = findEntry({ pid: vm.pid });
6616
- if (cur) {
6617
- writeEntry({ ...cur, name: candidate });
6618
- }
6619
- vm.name = candidate;
6620
- break;
6621
- }
6622
- }
6623
- }
6592
+ autoNameRestoredFork(vm, opts, meta);
6624
6593
  if (lazyPagesTotal !== void 0) {
6625
6594
  const cur = findEntry({ pid: vm.pid });
6626
6595
  if (cur) {
6627
6596
  writeEntry({
6628
6597
  ...cur,
6629
- lazyPagesTotal,
6630
- lazyPagesMountRoot: imgDir
6598
+ lazyPagesTotal
6631
6599
  });
6632
6600
  }
6633
6601
  }
@@ -6638,9 +6606,98 @@ async function restore(opts) {
6638
6606
  });
6639
6607
  return vm;
6640
6608
  }
6609
+ function resolveRestoreImage(opts, meta) {
6610
+ if (opts.image) {
6611
+ const resolved = resolve7(opts.cwd ?? process.cwd(), opts.image);
6612
+ if (!existsSync15(resolved)) {
6613
+ throw new BootError("BOOT_IMAGE_NOT_FOUND", `restore: image not found: ${resolved}`);
6614
+ }
6615
+ return resolved;
6616
+ }
6617
+ if (meta.sourceImage && existsSync15(meta.sourceImage)) {
6618
+ debugRestore("using meta.sourceImage path=%s", meta.sourceImage);
6619
+ return meta.sourceImage;
6620
+ }
6621
+ if (meta.sourceImage) {
6622
+ throw new BootError(
6623
+ "BOOT_IMAGE_NOT_FOUND",
6624
+ `restore: source image not found at ${meta.sourceImage}
6625
+ The snapshot was taken with this rootfs tarball, and the restore
6626
+ needs it as the guest's base rootfs.
6627
+ \u2022 copy the tarball to that path on this host, OR
6628
+ \u2022 pass an explicit override via the runtime's restore({ image })
6629
+ or the CLI's \`machinen restore --image <tarball>\`.`
6630
+ );
6631
+ }
6632
+ throw new BootError(
6633
+ "BOOT_IMAGE_NOT_FOUND",
6634
+ `restore: no rootfs image available for this bundle.
6635
+ The snapshot's meta.json doesn't record a source image (likely
6636
+ predates the field). Pass the same tarball you booted the
6637
+ source VM with via the runtime's restore({ image }) or the
6638
+ CLI's \`machinen restore --image <tarball>\`.`
6639
+ );
6640
+ }
6641
+ function autoNameRestoredFork(vm, opts, meta) {
6642
+ if (opts.name || !meta.sourceName) {
6643
+ return;
6644
+ }
6645
+ const candidates = [`${meta.sourceName}/${vm.pid}`, `${meta.sourceName}~${vm.pid}`];
6646
+ for (const candidate of candidates) {
6647
+ if (claimName(candidate, vm.pid)) {
6648
+ const cur = findEntry({ pid: vm.pid });
6649
+ if (cur) {
6650
+ writeEntry({ ...cur, name: candidate });
6651
+ }
6652
+ vm.name = candidate;
6653
+ break;
6654
+ }
6655
+ }
6656
+ }
6657
+ async function restoreVmstate(opts, snapDir) {
6658
+ const statePath = join14(snapDir, VMSTATE_FILE);
6659
+ const metaPath = join14(snapDir, "meta.json");
6660
+ let meta = { snappedAt: 0 };
6661
+ if (existsSync15(metaPath)) {
6662
+ try {
6663
+ meta = JSON.parse(readFileSync9(metaPath, "utf8"));
6664
+ } catch {
6665
+ }
6666
+ }
6667
+ const resolvedImage = resolveRestoreImage(opts, meta);
6668
+ const effectiveLiveMounts = resolveRestoreLiveMounts(meta.liveMounts, opts.liveMounts);
6669
+ let restoreMountDisk;
6670
+ if (meta.mountDisk) {
6671
+ const lowerAbs = join14(snapDir, meta.mountDisk.lower);
6672
+ const upperAbs = join14(snapDir, meta.mountDisk.upper);
6673
+ if (!existsSync15(lowerAbs) || !existsSync15(upperAbs)) {
6674
+ throw new BootError(
6675
+ "BOOT_SNAPSHOT_NOT_FOUND",
6676
+ `restore: bundle's mount overlay is missing one of:
6677
+ ${lowerAbs}
6678
+ ${upperAbs}`
6679
+ );
6680
+ }
6681
+ restoreMountDisk = { guest: meta.mountDisk.guest, lowerPath: lowerAbs, upperPath: upperAbs };
6682
+ }
6683
+ debugRestore("vmstate restore snapDir=%s state=%s image=%s", snapDir, statePath, resolvedImage);
6684
+ const vm = await boot({
6685
+ ...opts,
6686
+ image: resolvedImage,
6687
+ forkedFrom: snapDir,
6688
+ name: opts.name,
6689
+ liveMounts: effectiveLiveMounts,
6690
+ _restoreMountDisk: restoreMountDisk,
6691
+ _vmstateRestorePath: statePath
6692
+ });
6693
+ autoNameRestoredFork(vm, opts, meta);
6694
+ void setGuestHostname(vm, buildGuestHostname(vm.pid, vm.name)).catch(() => {
6695
+ });
6696
+ return vm;
6697
+ }
6641
6698
 
6642
6699
  // src/vm/fork.ts
6643
- var debugFork = debugLib14("machinen:fork");
6700
+ var debugFork = debugLib13("machinen:fork");
6644
6701
  async function performFork(ctx, opts) {
6645
6702
  await checkForkBackpressure({
6646
6703
  threshold: opts.freeMemoryThreshold ?? DEFAULT_FREE_MEMORY_THRESHOLD
@@ -6666,7 +6723,7 @@ async function performFork(ctx, opts) {
6666
6723
  } catch (err) {
6667
6724
  if (ephemeral) {
6668
6725
  try {
6669
- rmSync10(snapDir, { recursive: true, force: true });
6726
+ rmSync9(snapDir, { recursive: true, force: true });
6670
6727
  } catch {
6671
6728
  }
6672
6729
  }
@@ -6686,7 +6743,7 @@ async function performFork(ctx, opts) {
6686
6743
  } catch (err) {
6687
6744
  if (ephemeral) {
6688
6745
  try {
6689
- rmSync10(snapDir, { recursive: true, force: true });
6746
+ rmSync9(snapDir, { recursive: true, force: true });
6690
6747
  } catch {
6691
6748
  }
6692
6749
  }
@@ -6697,7 +6754,7 @@ async function performFork(ctx, opts) {
6697
6754
  void fork.wait().catch(() => {
6698
6755
  }).finally(() => {
6699
6756
  try {
6700
- rmSync10(snapDir, { recursive: true, force: true });
6757
+ rmSync9(snapDir, { recursive: true, force: true });
6701
6758
  debugFork("fork ephemeral bundle cleaned up snapDir=%s", snapDir);
6702
6759
  } catch (cleanupErr) {
6703
6760
  debugFork(
@@ -6719,7 +6776,7 @@ function measureFirstByte(vm) {
6719
6776
  }
6720
6777
 
6721
6778
  // src/vm/attach.ts
6722
- var debugAttach = debugLib15("machinen:attach");
6779
+ var debugAttach = debugLib14("machinen:attach");
6723
6780
  async function attach(opts) {
6724
6781
  debugAttach("attach lookup pid=%s name=%s", opts.pid ?? "<unset>", opts.name ?? "<unset>");
6725
6782
  const entry = findEntry(opts);
@@ -6740,7 +6797,7 @@ async function attach(opts) {
6740
6797
  const stderr = new PassThrough3();
6741
6798
  stdout.end();
6742
6799
  stderr.end();
6743
- const waitForExit2 = async () => {
6800
+ const waitForExit = async () => {
6744
6801
  while (isAlive(entry.pid)) {
6745
6802
  await new Promise((r) => setTimeout(r, 200));
6746
6803
  }
@@ -6753,7 +6810,7 @@ async function attach(opts) {
6753
6810
  stdout,
6754
6811
  stderr,
6755
6812
  async wait() {
6756
- return waitForExit2();
6813
+ return waitForExit();
6757
6814
  },
6758
6815
  async kill() {
6759
6816
  if (!isAlive(entry.pid)) {
@@ -6763,7 +6820,7 @@ async function attach(opts) {
6763
6820
  process.kill(entry.pid, "SIGKILL");
6764
6821
  } catch {
6765
6822
  }
6766
- await waitForExit2();
6823
+ await waitForExit();
6767
6824
  },
6768
6825
  async detach() {
6769
6826
  },
@@ -6806,7 +6863,8 @@ ${res.stderr}`
6806
6863
  };
6807
6864
  },
6808
6865
  async snapshot(snapshotOpts) {
6809
- if (!entry.diskPath) {
6866
+ const engine = resolveSnapshotEngine();
6867
+ if (engine === "criu" && !entry.diskPath || engine === "vmstate" && !entry.vmstatePath) {
6810
6868
  throw new SnapshotError(
6811
6869
  "SNAPSHOT_NO_DISK",
6812
6870
  "vm.snapshot: this VM was booted with `snapshot: false` (no scratch disk attached). Re-boot without that flag \u2014 the runtime will auto-allocate a sparse scratch \u2014 or pass `snapshot: '<path>'`."
@@ -6815,7 +6873,8 @@ ${res.stderr}`
6815
6873
  return performSnapshot(buildAttachSnapshotContext(), snapshotOpts);
6816
6874
  },
6817
6875
  async fork(forkOpts) {
6818
- if (!entry.diskPath) {
6876
+ const engine = resolveSnapshotEngine();
6877
+ if (engine === "criu" && !entry.diskPath || engine === "vmstate" && !entry.vmstatePath) {
6819
6878
  throw new SnapshotError(
6820
6879
  "SNAPSHOT_NO_DISK",
6821
6880
  "vm.fork: source VM has no scratch disk (booted with `snapshot: false`)."
@@ -6835,6 +6894,9 @@ ${res.stderr}`
6835
6894
  // bundle exactly like boot-owned snapshots do.
6836
6895
  mountDisk: entry.mountDisk,
6837
6896
  liveMounts: entry.liveMounts,
6897
+ // Vmstate engine: the VMM's whole-VM state-file path, persisted
6898
+ // at boot. performSnapshotVmstate SIGUSR1s the VMM and reads it.
6899
+ vmstatePath: entry.vmstatePath,
6838
6900
  execRaw: (cmd, execOpts) => handle.execRaw(cmd, execOpts),
6839
6901
  wait: () => handle.wait(),
6840
6902
  kill: () => handle.kill(),
@@ -6856,8 +6918,8 @@ var _internal = {
6856
6918
  };
6857
6919
 
6858
6920
  // src/provision.ts
6859
- var debug13 = debugLib16("machinen:provision");
6860
- var vmmDebug2 = debugLib16("machinen:vmm");
6921
+ var debug12 = debugLib15("machinen:provision");
6922
+ var vmmDebug2 = debugLib15("machinen:vmm");
6861
6923
  var TAR_TO_DISK_CMD = [
6862
6924
  "tar",
6863
6925
  "-C /",
@@ -6917,7 +6979,7 @@ function resolveBaseDtb(explicit, cwd = process.cwd()) {
6917
6979
  function resolveBaseAsset(spec, explicit, cwd) {
6918
6980
  if (explicit) {
6919
6981
  const abs = resolve9(cwd, explicit);
6920
- if (!existsSync17(abs)) {
6982
+ if (!existsSync16(abs)) {
6921
6983
  throw new ProvisionError(spec.missingCode, `${spec.kind} not found: ${abs}`);
6922
6984
  }
6923
6985
  return abs;
@@ -6925,7 +6987,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6925
6987
  const assetsDir = process.env.MACHINEN_ASSETS_DIR;
6926
6988
  if (assetsDir) {
6927
6989
  const p = resolve9(assetsDir, spec.assetsDirName);
6928
- if (!existsSync17(p)) {
6990
+ if (!existsSync16(p)) {
6929
6991
  throw new ProvisionError(
6930
6992
  "PROVISION_ASSETS_DIR_INVALID",
6931
6993
  `MACHINEN_ASSETS_DIR=${assetsDir} does not contain ${spec.assetsDirName}`
@@ -6934,7 +6996,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6934
6996
  return p;
6935
6997
  }
6936
6998
  const cached = join16(cliCachedBaseDir(), spec.cliCacheName);
6937
- if (existsSync17(cached)) {
6999
+ if (existsSync16(cached)) {
6938
7000
  return cached;
6939
7001
  }
6940
7002
  throw new ProvisionError(
@@ -6947,7 +7009,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6947
7009
  }
6948
7010
  function cliCachedBaseDir() {
6949
7011
  const pkgPath = resolve9(import.meta.dirname, "..", "package.json");
6950
- const version = JSON.parse(readFileSync11(pkgPath, "utf8")).version;
7012
+ const version = JSON.parse(readFileSync10(pkgPath, "utf8")).version;
6951
7013
  return join16(homedir8(), ".machinen", `runtime-v${version}`, "bases", "debian-arm64");
6952
7014
  }
6953
7015
  async function provision(opts) {
@@ -6956,18 +7018,18 @@ async function provision(opts) {
6956
7018
  const kernelAbs = resolveBaseKernel(opts.kernel, cwd);
6957
7019
  const dtbAbs = resolveBaseDtb(opts.dtb, cwd);
6958
7020
  const outAbs = resolve9(cwd, opts.out);
6959
- mkdirSync12(dirname9(outAbs), { recursive: true });
7021
+ mkdirSync12(dirname7(outAbs), { recursive: true });
6960
7022
  const t0 = Date.now();
6961
7023
  const workDir = mkdtempSync8(join16(tmpdir8(), "machinen-provision-"));
6962
7024
  const diskPath = join16(workDir, "scratch.img");
6963
7025
  const rootDiskPath = join16(workDir, "rootfs.img");
6964
7026
  const udsPath = join16(workDir, "exec.sock");
6965
- debug13("provision start base=%s out=%s workDir=%s", baseAbs, outAbs, workDir);
7027
+ debug12("provision start base=%s out=%s workDir=%s", baseAbs, outAbs, workDir);
6966
7028
  const phases = new PhaseTimer();
6967
7029
  try {
6968
7030
  const scratchBytes = opts.scratchDiskSizeBytes ?? 1024 * 1024 * 1024;
6969
7031
  allocateSparseFile4(diskPath, scratchBytes);
6970
- debug13("scratch disk allocated path=%s sizeBytes=%d", diskPath, scratchBytes);
7032
+ debug12("scratch disk allocated path=%s sizeBytes=%d", diskPath, scratchBytes);
6971
7033
  phases.start("rootdisk-prep");
6972
7034
  const cachedImg = ensureRootfsImage(baseAbs, {
6973
7035
  onPhase: (name, ms) => phases.mark(`rootdisk-prep.${name}`, ms)
@@ -6975,7 +7037,7 @@ async function provision(opts) {
6975
7037
  const reflinkT0 = Date.now();
6976
7038
  reflinkCopy(cachedImg, rootDiskPath);
6977
7039
  phases.mark("rootdisk-prep.reflink", Date.now() - reflinkT0);
6978
- debug13("rootdisk cloned src=%s dst=%s", cachedImg, rootDiskPath);
7040
+ debug12("rootdisk cloned src=%s dst=%s", cachedImg, rootDiskPath);
6979
7041
  markRootfsImageClean(cachedImg);
6980
7042
  phases.end("rootdisk-prep");
6981
7043
  phases.start("boot");
@@ -7021,14 +7083,14 @@ async function provision(opts) {
7021
7083
  const stderrTail = () => Buffer.concat(tailBuf).slice(-TAIL_MAX).toString("utf8");
7022
7084
  try {
7023
7085
  const installT0 = Date.now();
7024
- debug13("install hook entry");
7086
+ debug12("install hook entry");
7025
7087
  phases.start("install");
7026
7088
  try {
7027
7089
  await opts.install(vm);
7028
7090
  } catch (err) {
7029
7091
  const tail = stderrTail();
7030
7092
  const msg = err instanceof Error ? err.message : String(err);
7031
- debug13("install hook failed err=%s tailBytes=%d", msg, tail.length);
7093
+ debug12("install hook failed err=%s tailBytes=%d", msg, tail.length);
7032
7094
  throw new ProvisionError(
7033
7095
  "PROVISION_INSTALL_HOOK_FAILED",
7034
7096
  `install hook failed: ${msg}
@@ -7038,8 +7100,8 @@ ${tail}`,
7038
7100
  );
7039
7101
  }
7040
7102
  phases.end("install");
7041
- debug13("install hook done elapsed=%dms", Date.now() - installT0);
7042
- debug13("tar / -> /dev/vdb starting");
7103
+ debug12("install hook done elapsed=%dms", Date.now() - installT0);
7104
+ debug12("tar / -> /dev/vdb starting");
7043
7105
  const tarT0 = Date.now();
7044
7106
  phases.start("tar-to-disk");
7045
7107
  const tar = await VsockExec.run(udsPath, TAR_TO_DISK_CMD, {
@@ -7047,7 +7109,7 @@ ${tail}`,
7047
7109
  ...tapExecForLog(TAR_TO_DISK_CMD, opts.onLog)
7048
7110
  });
7049
7111
  phases.end("tar-to-disk");
7050
- debug13("tar / -> /dev/vdb done exit=%d elapsed=%dms", tar.exitCode, Date.now() - tarT0);
7112
+ debug12("tar / -> /dev/vdb done exit=%d elapsed=%dms", tar.exitCode, Date.now() - tarT0);
7051
7113
  if (tar.exitCode !== 0) {
7052
7114
  throw new ProvisionError(
7053
7115
  "PROVISION_DISK_TOO_SMALL",
@@ -7056,7 +7118,7 @@ Bump scratchDiskSizeBytes. stderr:
7056
7118
  ${tar.stderr}`
7057
7119
  );
7058
7120
  }
7059
- debug13("requesting guest poweroff");
7121
+ debug12("requesting guest poweroff");
7060
7122
  phases.start("poweroff-wait");
7061
7123
  await VsockExec.run(udsPath, "/sbin/machinen-poweroff", {
7062
7124
  connectTimeoutMs: 2e3,
@@ -7066,7 +7128,7 @@ ${tar.stderr}`
7066
7128
  console.error("provision: waiting for guest exit\u2026");
7067
7129
  await vm.wait();
7068
7130
  phases.end("poweroff-wait");
7069
- debug13("guest exited");
7131
+ debug12("guest exited");
7070
7132
  } finally {
7071
7133
  clearTimeout(killTimer);
7072
7134
  if (vm.pid > 0) {
@@ -7074,7 +7136,7 @@ ${tar.stderr}`
7074
7136
  });
7075
7137
  }
7076
7138
  }
7077
- debug13("repack disk tar -> %s starting", outAbs);
7139
+ debug12("repack disk tar -> %s starting", outAbs);
7078
7140
  const repackT0 = Date.now();
7079
7141
  phases.start("repack-targz");
7080
7142
  repackDiskTarToGz(diskPath, outAbs, {
@@ -7083,7 +7145,7 @@ ${tar.stderr}`
7083
7145
  onPhase: (name, ms) => phases.mark(`repack-targz.${name}`, ms)
7084
7146
  });
7085
7147
  phases.end("repack-targz");
7086
- debug13("repack done elapsed=%dms", Date.now() - repackT0);
7148
+ debug12("repack done elapsed=%dms", Date.now() - repackT0);
7087
7149
  const sizeBytes = statSync11(outAbs).size;
7088
7150
  warmImageConfigCache(
7089
7151
  outAbs,
@@ -7093,8 +7155,8 @@ ${tar.stderr}`
7093
7155
  } : null
7094
7156
  );
7095
7157
  const elapsedMs = Date.now() - t0;
7096
- debug13("provision complete sizeBytes=%d totalElapsed=%dms", sizeBytes, elapsedMs);
7097
- phases.flush(debug13, "provision", elapsedMs);
7158
+ debug12("provision complete sizeBytes=%d totalElapsed=%dms", sizeBytes, elapsedMs);
7159
+ phases.flush(debug12, "provision", elapsedMs);
7098
7160
  opts.onLog?.(phases.toEvent("provision", elapsedMs));
7099
7161
  return {
7100
7162
  imagePath: outAbs,
@@ -7103,7 +7165,7 @@ ${tar.stderr}`
7103
7165
  };
7104
7166
  } finally {
7105
7167
  try {
7106
- rmSync11(workDir, { recursive: true, force: true });
7168
+ rmSync10(workDir, { recursive: true, force: true });
7107
7169
  } catch {
7108
7170
  }
7109
7171
  }
@@ -7154,7 +7216,7 @@ function repackDiskTarToGz(diskPath, outAbs, opts = {}) {
7154
7216
  });
7155
7217
  } finally {
7156
7218
  try {
7157
- rmSync11(extractDir, { recursive: true, force: true });
7219
+ rmSync10(extractDir, { recursive: true, force: true });
7158
7220
  } catch {
7159
7221
  }
7160
7222
  }