@machinen/runtime 0.2.0 → 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 {
@@ -1158,19 +1274,19 @@ function connectOnceWithTimeout(udsPath, timeoutMs) {
1158
1274
  import { execFileSync as execFileSync9 } from "child_process";
1159
1275
  import {
1160
1276
  closeSync as closeSync7,
1161
- existsSync as existsSync17,
1277
+ existsSync as existsSync16,
1162
1278
  mkdirSync as mkdirSync12,
1163
1279
  mkdtempSync as mkdtempSync8,
1164
1280
  openSync as openSync7,
1165
- readFileSync as readFileSync11,
1166
- rmSync as rmSync11,
1281
+ readFileSync as readFileSync10,
1282
+ rmSync as rmSync10,
1167
1283
  statSync as statSync11,
1168
1284
  writeFileSync as writeFileSync9,
1169
1285
  writeSync as writeSync6
1170
1286
  } from "fs";
1171
1287
  import { homedir as homedir8, tmpdir as tmpdir8 } from "os";
1172
- import { dirname as dirname9, join as join16, resolve as resolve9 } from "path";
1173
- import debugLib16 from "debug";
1288
+ import { dirname as dirname7, join as join16, resolve as resolve9 } from "path";
1289
+ import debugLib15 from "debug";
1174
1290
 
1175
1291
  // src/phase-timer.ts
1176
1292
  var PhaseTimer = class {
@@ -1271,7 +1387,7 @@ function reflinkCopy(src, dst) {
1271
1387
  }
1272
1388
 
1273
1389
  // src/vm/attach.ts
1274
- import debugLib15 from "debug";
1390
+ import debugLib14 from "debug";
1275
1391
 
1276
1392
  // src/balloon-stats.ts
1277
1393
  import { readFileSync } from "fs";
@@ -1545,15 +1661,6 @@ function writeEntry(entry) {
1545
1661
  }
1546
1662
  debug2("write pid=%d name=%s sock=%s", entry.pid, entry.name ?? "<unset>", entry.socketPath);
1547
1663
  }
1548
- function patchEntry(pid, patch) {
1549
- const existing = readEntry(pid);
1550
- if (!existing) {
1551
- debug2("patch pid=%d skipped (no entry)", pid);
1552
- return;
1553
- }
1554
- const merged = { ...existing, ...patch, pid: existing.pid };
1555
- writeEntry(merged);
1556
- }
1557
1664
  function removeEntry(pid) {
1558
1665
  debug2("remove pid=%d", pid);
1559
1666
  const root = registryRoot();
@@ -1822,10 +1929,10 @@ function findEntry(query) {
1822
1929
  }
1823
1930
 
1824
1931
  // src/vm/fork.ts
1825
- import { mkdtempSync as mkdtempSync7, rmSync as rmSync10 } from "fs";
1932
+ import { mkdtempSync as mkdtempSync7, rmSync as rmSync9 } from "fs";
1826
1933
  import { tmpdir as tmpdir7 } from "os";
1827
1934
  import { join as join15, resolve as resolve8 } from "path";
1828
- import debugLib14 from "debug";
1935
+ import debugLib13 from "debug";
1829
1936
 
1830
1937
  // src/host-mem.ts
1831
1938
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1896,17 +2003,17 @@ import { execFileSync as execFileSync8 } from "child_process";
1896
2003
  import { randomBytes as randomBytes4 } from "crypto";
1897
2004
  import {
1898
2005
  closeSync as closeSync6,
1899
- existsSync as existsSync16,
2006
+ existsSync as existsSync15,
1900
2007
  openSync as openSync6,
1901
2008
  readdirSync as readdirSync7,
1902
- readFileSync as readFileSync10,
2009
+ readFileSync as readFileSync9,
1903
2010
  statSync as statSync10,
1904
- unlinkSync as unlinkSync8,
2011
+ unlinkSync as unlinkSync9,
1905
2012
  writeSync as writeSync5
1906
2013
  } from "fs";
1907
2014
  import { tmpdir as tmpdir6 } from "os";
1908
2015
  import { join as join14, resolve as resolve7 } from "path";
1909
- import debugLib13 from "debug";
2016
+ import debugLib12 from "debug";
1910
2017
 
1911
2018
  // src/lazy-pagemap.ts
1912
2019
  import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
@@ -2186,21 +2293,21 @@ function skipField(buf, pos, wireType) {
2186
2293
  }
2187
2294
 
2188
2295
  // src/vm/boot.ts
2189
- import { spawn as nodeSpawn4 } from "child_process";
2296
+ import { spawn as nodeSpawn3 } from "child_process";
2190
2297
  import { randomBytes as randomBytes3 } from "crypto";
2191
2298
  import { once } from "events";
2192
2299
  import {
2193
2300
  closeSync as closeSync5,
2194
- existsSync as existsSync15,
2301
+ existsSync as existsSync14,
2195
2302
  mkdtempSync as mkdtempSync6,
2196
2303
  openSync as openSync5,
2197
- rmSync as rmSync9,
2198
- unlinkSync as unlinkSync7,
2304
+ rmSync as rmSync8,
2305
+ unlinkSync as unlinkSync8,
2199
2306
  writeSync as writeSync4
2200
2307
  } from "fs";
2201
2308
  import { tmpdir as tmpdir5 } from "os";
2202
2309
  import { join as join13, resolve as resolve6 } from "path";
2203
- import debugLib12 from "debug";
2310
+ import debugLib11 from "debug";
2204
2311
 
2205
2312
  // src/detached-log.ts
2206
2313
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
@@ -2330,7 +2437,7 @@ import { homedir as homedir3, platform as osPlatform3 } from "os";
2330
2437
  import { dirname as dirname4, join as join5 } from "path";
2331
2438
  import debugLib4 from "debug";
2332
2439
  var debug4 = debugLib4("machinen:pdeathsig");
2333
- var PDEATHSIG_VERSION = "v3";
2440
+ var PDEATHSIG_VERSION = "v4";
2334
2441
  var warnedNoCompiler = false;
2335
2442
  var installInFlight = /* @__PURE__ */ new Map();
2336
2443
  var PDEATHSIG_C_SOURCE = `// pdeathsig: tiny exec wrapper that arranges for the target to die
@@ -2525,11 +2632,19 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2525
2632
  // the default disposition would kill the guard before kqueue
2526
2633
  // delivers the event, leaving the target alive and orphaned to
2527
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.
2528
2642
  sigset_t mask;
2529
2643
  sigemptyset(&mask);
2530
2644
  sigaddset(&mask, SIGTERM);
2531
2645
  sigaddset(&mask, SIGINT);
2532
2646
  sigaddset(&mask, SIGHUP);
2647
+ sigaddset(&mask, SIGUSR1);
2533
2648
  sigprocmask(SIG_BLOCK, &mask, NULL);
2534
2649
 
2535
2650
  pid_t child = fork();
@@ -2554,7 +2669,7 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2554
2669
  return reap_then_exit(child, 127);
2555
2670
  }
2556
2671
 
2557
- struct kevent changes[5];
2672
+ struct kevent changes[6];
2558
2673
  EV_SET(&changes[0], watch_pid, EVFILT_PROC, EV_ADD | EV_ONESHOT,
2559
2674
  NOTE_EXIT, 0, NULL);
2560
2675
  EV_SET(&changes[1], child, EVFILT_PROC, EV_ADD | EV_ONESHOT,
@@ -2562,7 +2677,8 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2562
2677
  EV_SET(&changes[2], SIGTERM, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2563
2678
  EV_SET(&changes[3], SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2564
2679
  EV_SET(&changes[4], SIGHUP, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
2565
- 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) {
2566
2682
  perror("pdeathsig: kevent register");
2567
2683
  kill(child, SIGTERM);
2568
2684
  return reap_then_exit(child, 127);
@@ -2587,6 +2703,13 @@ static int run_watch_pid_macos(pid_t watch_pid, char **target) {
2587
2703
  }
2588
2704
  if (n == 0) continue;
2589
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
+ }
2590
2713
  // Runtime is signaling us (typical: child.kill('SIGTERM')
2591
2714
  // from the Node side, or Ctrl-C). Forward to the target
2592
2715
  // and reap. Keep the original signal so callers that look
@@ -3161,201 +3284,19 @@ function warnGvproxyMissing() {
3161
3284
  );
3162
3285
  }
3163
3286
 
3164
- // src/mount-server-detached.ts
3165
- import { spawn as nodeSpawn2 } from "child_process";
3166
- import { existsSync as existsSync7, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
3167
- import { fileURLToPath } from "url";
3168
- import debugLib6 from "debug";
3169
- var debug6 = debugLib6("machinen:mount-server-detached");
3170
- async function spawnDetachedMountServer(opts) {
3171
- if (!Number.isInteger(opts.vmmPid) || opts.vmmPid <= 0) {
3172
- throw new MountError(
3173
- "MOUNT_SERVER_SPAWN_FAILED",
3174
- `spawnDetachedMountServer: invalid vmmPid ${opts.vmmPid}`
3175
- );
3176
- }
3177
- const resolved = resolveMountServerCommand();
3178
- const helperArgs = [
3179
- ...resolved.args,
3180
- "--uds",
3181
- opts.udsPath,
3182
- "--root",
3183
- opts.rootAbs,
3184
- "--mode",
3185
- opts.mode,
3186
- "--stats",
3187
- opts.statsPath
3188
- ];
3189
- const pdeathsigBin = await ensurePdeathsig();
3190
- const wrapped = wrapWithPdeathsig(pdeathsigBin, resolved.command, helperArgs, {
3191
- watchPid: opts.vmmPid
3192
- });
3193
- debug6(
3194
- "spawn uds=%s root=%s mode=%s vmm=%d shim=%s",
3195
- opts.udsPath,
3196
- opts.rootAbs,
3197
- opts.mode,
3198
- opts.vmmPid,
3199
- pdeathsigBin ? "yes" : "no"
3200
- );
3201
- const child = nodeSpawn2(wrapped.command, wrapped.args, {
3202
- stdio: ["ignore", "pipe", "pipe"]
3203
- });
3204
- child.unref();
3205
- child.stderr?.on("data", (chunk) => {
3206
- debug6("stderr: %s", chunk.toString().trimEnd());
3207
- });
3208
- child.stdout?.on("data", () => {
3209
- });
3210
- await waitForSocketOrExit(child, opts.udsPath);
3211
- const pid = child.pid;
3212
- if (pid === void 0 || pid <= 0) {
3213
- throw new MountError(
3214
- "MOUNT_SERVER_SPAWN_FAILED",
3215
- "spawnDetachedMountServer: helper started but pid is unset"
3216
- );
3217
- }
3218
- return makeHandle(child, pid, wrapped.command, opts);
3219
- }
3220
- function makeHandle(child, pid, exe, opts) {
3221
- let stopped = false;
3222
- return {
3223
- pid,
3224
- exe,
3225
- bytesServedOnPagesImg: () => readBytesServed(opts.statsPath),
3226
- stop: async () => {
3227
- if (stopped) {
3228
- return;
3229
- }
3230
- stopped = true;
3231
- if (child.exitCode === null && child.signalCode === null) {
3232
- try {
3233
- child.kill("SIGTERM");
3234
- } catch {
3235
- }
3236
- }
3237
- await waitForExit(child, 5e3);
3238
- for (const path of [opts.statsPath, opts.udsPath]) {
3239
- try {
3240
- rmSync4(path, { force: true });
3241
- } catch {
3242
- }
3243
- }
3244
- }
3245
- };
3246
- }
3247
- function readBytesServed(statsPath) {
3248
- try {
3249
- const raw = readFileSync7(statsPath, "utf8");
3250
- const parsed = JSON.parse(raw);
3251
- if (parsed && typeof parsed === "object" && "bytesServedOnPagesImg" in parsed && typeof parsed.bytesServedOnPagesImg === "number") {
3252
- return parsed.bytesServedOnPagesImg;
3253
- }
3254
- return 0;
3255
- } catch {
3256
- return 0;
3257
- }
3258
- }
3259
- async function waitForExit(child, timeoutMs) {
3260
- if (child.exitCode !== null || child.signalCode !== null) {
3261
- return;
3262
- }
3263
- await new Promise((resolve10) => {
3264
- const timer = setTimeout(() => {
3265
- try {
3266
- child.kill("SIGKILL");
3267
- } catch {
3268
- }
3269
- resolve10();
3270
- }, timeoutMs);
3271
- timer.unref();
3272
- child.once("exit", () => {
3273
- clearTimeout(timer);
3274
- resolve10();
3275
- });
3276
- });
3277
- }
3278
- async function waitForSocketOrExit(child, udsPath) {
3279
- const deadline = Date.now() + 3e3;
3280
- let earlyExitErr = null;
3281
- const onExit = (code, signal) => {
3282
- earlyExitErr = new Error(
3283
- `mount-server helper exited before socket was ready (code=${code} signal=${signal})`
3284
- );
3285
- };
3286
- child.once("exit", onExit);
3287
- try {
3288
- while (Date.now() < deadline) {
3289
- if (existsSync7(udsPath)) {
3290
- return;
3291
- }
3292
- if (earlyExitErr) {
3293
- throw earlyExitErr;
3294
- }
3295
- await new Promise((r) => setTimeout(r, 25));
3296
- }
3297
- if (earlyExitErr) {
3298
- throw earlyExitErr;
3299
- }
3300
- try {
3301
- child.kill("SIGKILL");
3302
- } catch {
3303
- }
3304
- throw new MountError(
3305
- "MOUNT_SERVER_SPAWN_FAILED",
3306
- `mount-server helper did not bind ${udsPath} within 3000ms`
3307
- );
3308
- } finally {
3309
- child.removeListener("exit", onExit);
3310
- }
3311
- }
3312
- var cachedCommand = null;
3313
- function resolveMountServerCommand() {
3314
- if (cachedCommand) {
3315
- return cachedCommand;
3316
- }
3317
- const override = process.env.MACHINEN_MOUNT_SERVER_BIN;
3318
- if (override) {
3319
- if (!existsSync7(override)) {
3320
- throw new MountError(
3321
- "MOUNT_SERVER_BIN_MISSING",
3322
- `MACHINEN_MOUNT_SERVER_BIN=${override} does not exist`
3323
- );
3324
- }
3325
- cachedCommand = override.endsWith(".ts") ? { command: process.execPath, args: ["--import", "tsx", override] } : { command: process.execPath, args: [override] };
3326
- return cachedCommand;
3327
- }
3328
- const distUrl = new URL("./mount-server-bin.js", import.meta.url);
3329
- const distPath = fileURLToPath(distUrl);
3330
- if (existsSync7(distPath)) {
3331
- cachedCommand = { command: process.execPath, args: [distPath] };
3332
- return cachedCommand;
3333
- }
3334
- const srcUrl = new URL("./mount-server-bin.ts", import.meta.url);
3335
- const srcPath = fileURLToPath(srcUrl);
3336
- if (existsSync7(srcPath)) {
3337
- cachedCommand = { command: process.execPath, args: ["--import", "tsx", srcPath] };
3338
- return cachedCommand;
3339
- }
3340
- throw new MountError(
3341
- "MOUNT_SERVER_BIN_MISSING",
3342
- `mount-server bin not found at ${distPath} or ${srcPath}. Run pnpm build, or set MACHINEN_MOUNT_SERVER_BIN to an absolute path.`
3343
- );
3344
- }
3345
-
3346
3287
  // src/rootfs-img.ts
3347
3288
  import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "child_process";
3348
3289
  import { createHash } from "crypto";
3349
3290
  import {
3350
3291
  closeSync as closeSync2,
3351
- existsSync as existsSync8,
3292
+ existsSync as existsSync7,
3352
3293
  fsyncSync as fsyncSync2,
3353
3294
  mkdirSync as mkdirSync6,
3354
3295
  mkdtempSync as mkdtempSync2,
3355
3296
  openSync as openSync2,
3356
3297
  readSync,
3357
3298
  renameSync as renameSync3,
3358
- rmSync as rmSync5,
3299
+ rmSync as rmSync4,
3359
3300
  statSync as statSync4,
3360
3301
  truncateSync,
3361
3302
  unlinkSync as unlinkSync5,
@@ -3363,9 +3304,9 @@ import {
3363
3304
  } from "fs";
3364
3305
  import { createRequire as createRequire2 } from "module";
3365
3306
  import { arch, homedir as homedir5, platform as platform3 } from "os";
3366
- import { dirname as dirname6, join as join7, resolve } from "path";
3367
- import debugLib7 from "debug";
3368
- 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");
3369
3310
  function rootfsImgCacheDir() {
3370
3311
  return join7(homedir5(), ".cache", "machinen", "rootfs");
3371
3312
  }
@@ -3373,7 +3314,7 @@ function okMarkerPath(imgPath) {
3373
3314
  return `${imgPath}.ok`;
3374
3315
  }
3375
3316
  function markRootfsImageClean(imgPath) {
3376
- if (!existsSync8(imgPath)) {
3317
+ if (!existsSync7(imgPath)) {
3377
3318
  return;
3378
3319
  }
3379
3320
  const okPath = okMarkerPath(imgPath);
@@ -3386,7 +3327,7 @@ function markRootfsImageClean(imgPath) {
3386
3327
  fd = -1;
3387
3328
  renameSync3(tmp, okPath);
3388
3329
  } catch (err) {
3389
- debug7("markRootfsImageClean failed img=%s err=%s", imgPath, err.message);
3330
+ debug6("markRootfsImageClean failed img=%s err=%s", imgPath, err.message);
3390
3331
  if (fd >= 0) {
3391
3332
  try {
3392
3333
  closeSync2(fd);
@@ -3401,7 +3342,7 @@ function markRootfsImageClean(imgPath) {
3401
3342
  }
3402
3343
  function ensureRootfsImage(tarPath, opts = {}) {
3403
3344
  const tarAbs = resolve(tarPath);
3404
- if (!existsSync8(tarAbs)) {
3345
+ if (!existsSync7(tarAbs)) {
3405
3346
  throw new ProvisionError(
3406
3347
  "PROVISION_BASE_NOT_FOUND",
3407
3348
  `ensureRootfsImage: tarball not found at ${tarAbs}`
@@ -3414,10 +3355,10 @@ function ensureRootfsImage(tarPath, opts = {}) {
3414
3355
  opts.onPhase?.("sha256", Date.now() - shaT0);
3415
3356
  const imgPath = join7(cacheDir, `${sha}.img`);
3416
3357
  const okPath = okMarkerPath(imgPath);
3417
- if (!opts.force && existsSync8(imgPath)) {
3418
- debug7("cache hit sha=%s img=%s", sha.slice(0, 12), imgPath);
3419
- if (!existsSync8(okPath)) {
3420
- 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);
3421
3362
  } else {
3422
3363
  try {
3423
3364
  unlinkSync5(okPath);
@@ -3433,21 +3374,21 @@ function ensureRootfsImage(tarPath, opts = {}) {
3433
3374
  const cur = statSync4(imgPath).size;
3434
3375
  if (opts.sizeBytes > cur) {
3435
3376
  truncateSync(imgPath, opts.sizeBytes);
3436
- 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);
3437
3378
  }
3438
3379
  } catch (err) {
3439
- 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);
3440
3381
  }
3441
3382
  opts.onPhase?.("sparse-extend", Date.now() - truncT0);
3442
3383
  }
3443
3384
  return imgPath;
3444
3385
  }
3445
- debug7("cache hit unusable, will rematerialize img=%s", imgPath);
3386
+ debug6("cache hit unusable, will rematerialize img=%s", imgPath);
3446
3387
  }
3447
3388
  }
3448
3389
  if (!opts.force) {
3449
3390
  const sibling = siblingPrebakePath(tarAbs);
3450
- if (sibling && existsSync8(sibling)) {
3391
+ if (sibling && existsSync7(sibling)) {
3451
3392
  const prebakeT0 = Date.now();
3452
3393
  const fast = tryPrebakeFromSibling({
3453
3394
  sibling,
@@ -3475,7 +3416,7 @@ function ensureRootfsImage(tarPath, opts = {}) {
3475
3416
  const stagingImg = join7(stagingDir, "rootfs.img");
3476
3417
  mkdirSync6(stagingTree, { recursive: true });
3477
3418
  try {
3478
- debug7("materialize sha=%s tar=%s", sha.slice(0, 12), tarAbs);
3419
+ debug6("materialize sha=%s tar=%s", sha.slice(0, 12), tarAbs);
3479
3420
  const extractT0 = Date.now();
3480
3421
  extractTarball(tarAbs, stagingTree);
3481
3422
  opts.onPhase?.("tar-extract", Date.now() - extractT0);
@@ -3483,7 +3424,7 @@ function ensureRootfsImage(tarPath, opts = {}) {
3483
3424
  const multiplier = opts.sizeMultiplier ?? 2.5;
3484
3425
  const minBytes = opts.minSizeBytes ?? 2 * 1024 * 1024 * 1024;
3485
3426
  const sizeBytes = opts.sizeBytes ?? Math.max(minBytes, Math.ceil(treeBytes * multiplier));
3486
- debug7(
3427
+ debug6(
3487
3428
  "size tree=%d size=%d multiplier=%s explicit=%s",
3488
3429
  treeBytes,
3489
3430
  sizeBytes,
@@ -3506,11 +3447,11 @@ function ensureRootfsImage(tarPath, opts = {}) {
3506
3447
  );
3507
3448
  }
3508
3449
  renameSync3(stagingImg, imgPath);
3509
- 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);
3510
3451
  return imgPath;
3511
3452
  } finally {
3512
3453
  try {
3513
- rmSync5(stagingDir, { recursive: true, force: true });
3454
+ rmSync4(stagingDir, { recursive: true, force: true });
3514
3455
  } catch {
3515
3456
  }
3516
3457
  }
@@ -3521,7 +3462,7 @@ function cachedImageIsUsable(imgPath) {
3521
3462
  }
3522
3463
  const e2fsck = whichFirst(["e2fsck"]);
3523
3464
  if (!e2fsck) {
3524
- debug7("no e2fsck on PATH; trusting cached img=%s", imgPath);
3465
+ debug6("no e2fsck on PATH; trusting cached img=%s", imgPath);
3525
3466
  return true;
3526
3467
  }
3527
3468
  const r = spawnSync2(e2fsck, ["-fy", imgPath], {
@@ -3530,7 +3471,7 @@ function cachedImageIsUsable(imgPath) {
3530
3471
  if (r.status === 0 || r.status === 1 || r.status === 2) {
3531
3472
  return true;
3532
3473
  }
3533
- debug7(
3474
+ debug6(
3534
3475
  "e2fsck rejected img=%s status=%s stderr=%s",
3535
3476
  imgPath,
3536
3477
  r.status,
@@ -3601,7 +3542,7 @@ function findKegOnlyE2fs(names, dirs = KEG_ONLY_E2FS_DIRS) {
3601
3542
  for (const dir of dirs) {
3602
3543
  for (const name of names) {
3603
3544
  const candidate = join7(dir, name);
3604
- if (existsSync8(candidate)) {
3545
+ if (existsSync7(candidate)) {
3605
3546
  return candidate;
3606
3547
  }
3607
3548
  }
@@ -3613,8 +3554,8 @@ function resolveMke2fsEnvOverride() {
3613
3554
  if (!envOverride) {
3614
3555
  return void 0;
3615
3556
  }
3616
- if (existsSync8(envOverride)) {
3617
- debug7("resolved via MACHINEN_MKE2FS=%s", envOverride);
3557
+ if (existsSync7(envOverride)) {
3558
+ debug6("resolved via MACHINEN_MKE2FS=%s", envOverride);
3618
3559
  return envOverride;
3619
3560
  }
3620
3561
  throw new ProvisionError(
@@ -3624,12 +3565,11 @@ function resolveMke2fsEnvOverride() {
3624
3565
  }
3625
3566
  var require_2 = createRequire2(import.meta.url);
3626
3567
  function findBundledMke2fs() {
3627
- const pkg = `@machinen/e2fsprogs-${arch()}-${platform3()}`;
3568
+ const pkg = `@machinen/native-${arch()}-${platform3()}`;
3628
3569
  try {
3629
- const pkgJson = require_2.resolve(`${pkg}/package.json`);
3630
- const candidate = join7(dirname6(pkgJson), "bin", "mke2fs");
3631
- if (existsSync8(candidate)) {
3632
- return candidate;
3570
+ const mod = require_2(pkg);
3571
+ if (mod.mke2fs && existsSync7(mod.mke2fs)) {
3572
+ return mod.mke2fs;
3633
3573
  }
3634
3574
  } catch {
3635
3575
  }
@@ -3658,7 +3598,7 @@ function tryPrebakeFromSibling(args) {
3658
3598
  const stagingDir = mkdtempSync2(join7(cacheDir, `${sha.slice(0, 12)}-prebake-`));
3659
3599
  const stagingImg = join7(stagingDir, "rootfs.img");
3660
3600
  try {
3661
- 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));
3662
3602
  const dstFd = openSync2(stagingImg, "w");
3663
3603
  let gunzipOk = false;
3664
3604
  try {
@@ -3667,7 +3607,7 @@ function tryPrebakeFromSibling(args) {
3667
3607
  });
3668
3608
  gunzipOk = r.status === 0;
3669
3609
  if (!gunzipOk) {
3670
- debug7(
3610
+ debug6(
3671
3611
  "prebake gunzip failed sibling=%s status=%s stderr=%s",
3672
3612
  sibling,
3673
3613
  r.status,
@@ -3681,7 +3621,7 @@ function tryPrebakeFromSibling(args) {
3681
3621
  return void 0;
3682
3622
  }
3683
3623
  if (!looksLikeExt4(stagingImg)) {
3684
- 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");
3685
3625
  return void 0;
3686
3626
  }
3687
3627
  if (sizeBytes !== void 0) {
@@ -3691,14 +3631,14 @@ function tryPrebakeFromSibling(args) {
3691
3631
  }
3692
3632
  }
3693
3633
  renameSync3(stagingImg, imgPath);
3694
- 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);
3695
3635
  return imgPath;
3696
3636
  } catch (err) {
3697
- debug7("prebake error sibling=%s err=%s", sibling, err.message);
3637
+ debug6("prebake error sibling=%s err=%s", sibling, err.message);
3698
3638
  return void 0;
3699
3639
  } finally {
3700
3640
  try {
3701
- rmSync5(stagingDir, { recursive: true, force: true });
3641
+ rmSync4(stagingDir, { recursive: true, force: true });
3702
3642
  } catch {
3703
3643
  }
3704
3644
  }
@@ -3733,7 +3673,7 @@ function prebakeRootfsImageFromTree(args) {
3733
3673
  try {
3734
3674
  const mke2fs = resolveMke2fs();
3735
3675
  if (!mke2fs) {
3736
- debug7("prebake skip: no mke2fs available");
3676
+ debug6("prebake skip: no mke2fs available");
3737
3677
  return;
3738
3678
  }
3739
3679
  mkdirSync6(cacheDir, { recursive: true });
@@ -3741,8 +3681,8 @@ function prebakeRootfsImageFromTree(args) {
3741
3681
  const sha = sha256OfFile(tarPath);
3742
3682
  onPhase?.("prebake.sha256", Date.now() - shaT0);
3743
3683
  const imgPath = join7(cacheDir, `${sha}.img`);
3744
- if (existsSync8(imgPath)) {
3745
- 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));
3746
3686
  return;
3747
3687
  }
3748
3688
  const stagingDir = mkdtempSync2(join7(cacheDir, `${sha.slice(0, 12)}-prebake-tree-`));
@@ -3760,7 +3700,7 @@ function prebakeRootfsImageFromTree(args) {
3760
3700
  );
3761
3701
  onPhase?.("prebake.mke2fs", Date.now() - mkT0);
3762
3702
  if (mk.status !== 0) {
3763
- debug7(
3703
+ debug6(
3764
3704
  "prebake mke2fs failed status=%s stderr=%s",
3765
3705
  mk.status,
3766
3706
  mk.stderr?.toString().slice(0, 200) ?? ""
@@ -3769,21 +3709,21 @@ function prebakeRootfsImageFromTree(args) {
3769
3709
  }
3770
3710
  renameSync3(stagingImg, imgPath);
3771
3711
  markRootfsImageClean(imgPath);
3772
- debug7("prebake emitted cache=%s sizeBytes=%d", imgPath, sizeBytes);
3712
+ debug6("prebake emitted cache=%s sizeBytes=%d", imgPath, sizeBytes);
3773
3713
  } finally {
3774
3714
  try {
3775
- rmSync5(stagingDir, { recursive: true, force: true });
3715
+ rmSync4(stagingDir, { recursive: true, force: true });
3776
3716
  } catch {
3777
3717
  }
3778
3718
  }
3779
3719
  } catch (err) {
3780
- debug7("prebake error err=%s", err.message);
3720
+ debug6("prebake error err=%s", err.message);
3781
3721
  }
3782
3722
  }
3783
3723
 
3784
3724
  // src/vm/bundle.ts
3785
3725
  import { randomBytes as randomBytes2 } from "crypto";
3786
- 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";
3787
3727
  import { tmpdir as tmpdir4 } from "os";
3788
3728
  import { join as join11, resolve as resolve4 } from "path";
3789
3729
 
@@ -3791,24 +3731,24 @@ import { join as join11, resolve as resolve4 } from "path";
3791
3731
  import { spawnSync as spawnSync3 } from "child_process";
3792
3732
  import {
3793
3733
  cpSync,
3794
- existsSync as existsSync9,
3734
+ existsSync as existsSync8,
3795
3735
  lstatSync,
3796
3736
  mkdirSync as mkdirSync7,
3797
3737
  mkdtempSync as mkdtempSync3,
3798
3738
  readdirSync as readdirSync4,
3799
- readFileSync as readFileSync8,
3739
+ readFileSync as readFileSync7,
3800
3740
  readlinkSync as readlinkSync2,
3801
- rmSync as rmSync6,
3741
+ rmSync as rmSync5,
3802
3742
  statSync as statSync5,
3803
3743
  writeFileSync as writeFileSync5
3804
3744
  } from "fs";
3805
3745
  import { createRequire as createRequire3 } from "module";
3806
3746
  import { arch as osArch2, platform as osPlatform5, tmpdir as tmpdir2 } from "os";
3807
- import { dirname as dirname7, join as join8 } from "path";
3808
- import { fileURLToPath as fileURLToPath2 } from "url";
3809
- import debugLib8 from "debug";
3747
+ import { dirname as dirname6, join as join8 } from "path";
3748
+ import { fileURLToPath } from "url";
3749
+ import debugLib7 from "debug";
3810
3750
  var require_3 = createRequire3(import.meta.url);
3811
- var debug8 = debugLib8("machinen:mkinitramfs");
3751
+ var debug7 = debugLib7("machinen:mkinitramfs");
3812
3752
  var DEFAULT_WORKSPACE_EXCLUDES = /* @__PURE__ */ new Set([
3813
3753
  ".git",
3814
3754
  "node_modules",
@@ -3868,7 +3808,7 @@ function newc(name, mode, opts = {}) {
3868
3808
  return out;
3869
3809
  }
3870
3810
  function loadExcludes(path) {
3871
- const raw = readFileSync8(path, "utf8");
3811
+ const raw = readFileSync7(path, "utf8");
3872
3812
  const out = [];
3873
3813
  for (const line of raw.split("\n")) {
3874
3814
  const stripped = line.split("#", 1)[0].trim();
@@ -3961,7 +3901,7 @@ function* walkRootfs(root, rel, excludes, counts) {
3961
3901
  yield newc(childRel, 16384 | m & 4095);
3962
3902
  yield* walkRootfs(root, childRel, excludes, counts);
3963
3903
  } else if (st.isFile()) {
3964
- yield newc(childRel, 32768 | m & 4095, { data: readFileSync8(childFull) });
3904
+ yield newc(childRel, 32768 | m & 4095, { data: readFileSync7(childFull) });
3965
3905
  }
3966
3906
  }
3967
3907
  }
@@ -3998,7 +3938,7 @@ function* walkWorkspace(root, rel, mountpoint, excludes, counts) {
3998
3938
  yield newc(arcName, 16384 | m & 4095);
3999
3939
  yield* walkWorkspace(root, childRel, mountpoint, excludes, counts);
4000
3940
  } else if (st.isFile()) {
4001
- const data = readFileSync8(childFull);
3941
+ const data = readFileSync7(childFull);
4002
3942
  counts.bytes += data.length;
4003
3943
  yield newc(arcName, 32768 | m & 4095, { data });
4004
3944
  }
@@ -4015,7 +3955,7 @@ function packBundle(opts) {
4015
3955
  throw new MkinitramfsError("MKINITRAMFS_BUNDLE_INVALID", `--bundle: missing ${cfgPath}`);
4016
3956
  }
4017
3957
  const needsMerge = Boolean(opts.base) || Boolean(opts.mount);
4018
- debug8(
3958
+ debug7(
4019
3959
  "packBundle bundle=%s out=%s base=%s mount=%s needsMerge=%s",
4020
3960
  opts.bundle,
4021
3961
  opts.out,
@@ -4031,13 +3971,13 @@ function packBundle(opts) {
4031
3971
  const extractT0 = Date.now();
4032
3972
  const res = spawnSync3("tar", ["-xzf", opts.base, "-C", mergeTmp]);
4033
3973
  if (res.status !== 0) {
4034
- rmSync6(mergeTmp, { recursive: true, force: true });
3974
+ rmSync5(mergeTmp, { recursive: true, force: true });
4035
3975
  throw new MkinitramfsError(
4036
3976
  "MKINITRAMFS_BASE_EXTRACT_FAILED",
4037
3977
  `tar -xzf ${opts.base} failed: ${res.stderr?.toString() ?? ""}`
4038
3978
  );
4039
3979
  }
4040
- debug8("base extracted elapsed=%dms", Date.now() - extractT0);
3980
+ debug7("base extracted elapsed=%dms", Date.now() - extractT0);
4041
3981
  }
4042
3982
  if (opts.mount) {
4043
3983
  overlayMount(mergeTmp, opts.mount.host, opts.mount.guest);
@@ -4057,8 +3997,7 @@ function packBundle(opts) {
4057
3997
  }
4058
3998
  appendFinalEntries(parts, {
4059
3999
  initPath: opts.initPath ?? defaultInitPath(),
4060
- config: patchConfigEnv(readFileSync8(cfgPath), opts.env),
4061
- fuseAgentPath: opts.fuseAgentPath,
4000
+ config: patchConfigEnv(readFileSync7(cfgPath), opts.env),
4062
4001
  // Always inject the fresh /init AFTER walking the rootfs.
4063
4002
  // `provision()` flows feed the previous run's frozen rootfs back
4064
4003
  // in as `base`, and that capture has /init at root. The walked
@@ -4079,7 +4018,7 @@ function packBundle(opts) {
4079
4018
  execAgentPath: opts.execAgentPath ?? defaultExecAgentPath()
4080
4019
  });
4081
4020
  writeFileSync5(opts.out, Buffer.concat(parts));
4082
- debug8(
4021
+ debug7(
4083
4022
  "packBundle done files=%d bytes=%d elapsed=%dms",
4084
4023
  counts.files,
4085
4024
  counts.bytes,
@@ -4087,7 +4026,7 @@ function packBundle(opts) {
4087
4026
  );
4088
4027
  } finally {
4089
4028
  if (mergeTmp) {
4090
- rmSync6(mergeTmp, { recursive: true, force: true });
4029
+ rmSync5(mergeTmp, { recursive: true, force: true });
4091
4030
  }
4092
4031
  }
4093
4032
  }
@@ -4103,7 +4042,7 @@ function patchConfigEnv(config, env) {
4103
4042
  function overlayMount(mergeRoot, hostAbs, guest) {
4104
4043
  const rel = guest.replace(/^\/+/, "");
4105
4044
  const dst = join8(mergeRoot, rel);
4106
- mkdirSync7(dirname7(dst), { recursive: true });
4045
+ mkdirSync7(dirname6(dst), { recursive: true });
4107
4046
  cpSync(hostAbs, dst, {
4108
4047
  recursive: true,
4109
4048
  force: true,
@@ -4120,13 +4059,12 @@ function packTinyBundle(opts) {
4120
4059
  parts.push(newc(".", 16877));
4121
4060
  appendFinalEntries(parts, {
4122
4061
  initPath: opts.initPath ?? defaultInitPath(),
4123
- config: patchConfigEnv(readFileSync8(cfgPath), opts.env),
4124
- fuseAgentPath: opts.fuseAgentPath,
4062
+ config: patchConfigEnv(readFileSync7(cfgPath), opts.env),
4125
4063
  injectInit: true,
4126
4064
  mountGuest: opts.mountGuest
4127
4065
  });
4128
4066
  writeFileSync5(opts.out, Buffer.concat(parts));
4129
- debug8("packTinyBundle done elapsed=%dms", Date.now() - t0);
4067
+ debug7("packTinyBundle done elapsed=%dms", Date.now() - t0);
4130
4068
  }
4131
4069
  function packRootfs(opts) {
4132
4070
  const counts = { files: 0, bytes: 0 };
@@ -4136,7 +4074,7 @@ function packRootfs(opts) {
4136
4074
  }
4137
4075
  appendFinalEntries(parts, {
4138
4076
  initPath: opts.initPath ?? defaultInitPath(),
4139
- config: opts.config ? readFileSync8(opts.config) : void 0,
4077
+ config: opts.config ? readFileSync7(opts.config) : void 0,
4140
4078
  injectInit: true
4141
4079
  });
4142
4080
  writeFileSync5(opts.out, Buffer.concat(parts));
@@ -4146,11 +4084,11 @@ function packMinimal(opts) {
4146
4084
  const parts = [
4147
4085
  newc(".", 16877),
4148
4086
  newc("dev", 16877),
4149
- newc("init", 33261, { data: readFileSync8(initPath) })
4087
+ newc("init", 33261, { data: readFileSync7(initPath) })
4150
4088
  ];
4151
4089
  appendFinalEntries(parts, {
4152
4090
  initPath,
4153
- config: opts.config ? readFileSync8(opts.config) : void 0,
4091
+ config: opts.config ? readFileSync7(opts.config) : void 0,
4154
4092
  injectInit: true
4155
4093
  });
4156
4094
  writeFileSync5(opts.out, Buffer.concat(parts));
@@ -4186,7 +4124,7 @@ function appendFinalEntries(parts, opts) {
4186
4124
  if (opts.injectInit) {
4187
4125
  let initBytes = null;
4188
4126
  try {
4189
- initBytes = readFileSync8(opts.initPath);
4127
+ initBytes = readFileSync7(opts.initPath);
4190
4128
  } catch (err) {
4191
4129
  if (process.env.MACHINEN_REQUIRE_FIXTURES !== "0") {
4192
4130
  throw new MkinitramfsError(
@@ -4202,18 +4140,11 @@ function appendFinalEntries(parts, opts) {
4202
4140
  }
4203
4141
  if (opts.execAgentPath) {
4204
4142
  try {
4205
- const bytes = readFileSync8(opts.execAgentPath);
4143
+ const bytes = readFileSync7(opts.execAgentPath);
4206
4144
  parts.push(newc("exec-agent", 33261, { data: bytes }));
4207
4145
  } catch {
4208
4146
  }
4209
4147
  }
4210
- if (opts.fuseAgentPath) {
4211
- try {
4212
- const bytes = readFileSync8(opts.fuseAgentPath);
4213
- parts.push(newc("fuse-agent", 33261, { data: bytes }));
4214
- } catch {
4215
- }
4216
- }
4217
4148
  if (opts.config) {
4218
4149
  parts.push(newc("machinen-config.json", 33188, { data: opts.config }));
4219
4150
  }
@@ -4240,24 +4171,22 @@ function resolveGuestPaths() {
4240
4171
  if (cachedGuestPaths) {
4241
4172
  return cachedGuestPaths;
4242
4173
  }
4243
- const pkgName = `@machinen/vmm-${osArch2()}-${osPlatform5()}`;
4174
+ const pkgName = `@machinen/native-${osArch2()}-${osPlatform5()}`;
4244
4175
  try {
4245
4176
  const mod = require_3(pkgName);
4246
- if (mod.initPath && mod.fuseAgentPath && mod.execAgentPath && existsSync9(mod.initPath)) {
4177
+ if (mod.initPath && mod.execAgentPath && existsSync8(mod.initPath)) {
4247
4178
  cachedGuestPaths = {
4248
4179
  initPath: mod.initPath,
4249
- fuseAgentPath: mod.fuseAgentPath,
4250
4180
  execAgentPath: mod.execAgentPath
4251
4181
  };
4252
4182
  return cachedGuestPaths;
4253
4183
  }
4254
4184
  } catch {
4255
4185
  }
4256
- const here = dirname7(fileURLToPath2(import.meta.url));
4186
+ const here = dirname6(fileURLToPath(import.meta.url));
4257
4187
  const fixtures = join8(here, "..", "..", "microvm", "test-fixtures");
4258
4188
  cachedGuestPaths = {
4259
4189
  initPath: join8(fixtures, "init"),
4260
- fuseAgentPath: join8(fixtures, "fuse-agent"),
4261
4190
  execAgentPath: join8(fixtures, "exec-agent")
4262
4191
  };
4263
4192
  return cachedGuestPaths;
@@ -4265,9 +4194,6 @@ function resolveGuestPaths() {
4265
4194
  function defaultInitPath() {
4266
4195
  return resolveGuestPaths().initPath;
4267
4196
  }
4268
- function defaultFuseAgentPath() {
4269
- return resolveGuestPaths().fuseAgentPath;
4270
- }
4271
4197
  function defaultExecAgentPath() {
4272
4198
  return resolveGuestPaths().execAgentPath;
4273
4199
  }
@@ -4363,7 +4289,7 @@ function takeFlag(args, flag) {
4363
4289
  return value;
4364
4290
  }
4365
4291
  function defaultOut() {
4366
- const here = dirname7(fileURLToPath2(import.meta.url));
4292
+ const here = dirname6(fileURLToPath(import.meta.url));
4367
4293
  return join8(here, "..", "..", "microvm", "test-fixtures", "initramfs.cpio");
4368
4294
  }
4369
4295
  function die(msg) {
@@ -4376,7 +4302,7 @@ import { execFileSync as execFileSync6, spawnSync as spawnSync4 } from "child_pr
4376
4302
  import { createHash as createHash2 } from "crypto";
4377
4303
  import {
4378
4304
  closeSync as closeSync3,
4379
- existsSync as existsSync10,
4305
+ existsSync as existsSync9,
4380
4306
  fsyncSync as fsyncSync3,
4381
4307
  mkdirSync as mkdirSync8,
4382
4308
  mkdtempSync as mkdtempSync4,
@@ -4385,7 +4311,7 @@ import {
4385
4311
  readdirSync as readdirSync5,
4386
4312
  readlinkSync as readlinkSync3,
4387
4313
  renameSync as renameSync4,
4388
- rmSync as rmSync7,
4314
+ rmSync as rmSync6,
4389
4315
  statSync as statSync6,
4390
4316
  truncateSync as truncateSync2,
4391
4317
  unlinkSync as unlinkSync6,
@@ -4394,9 +4320,9 @@ import {
4394
4320
  } from "fs";
4395
4321
  import { createRequire as createRequire4 } from "module";
4396
4322
  import { arch as arch2, homedir as homedir6, platform as platform4, tmpdir as tmpdir3 } from "os";
4397
- import { dirname as dirname8, join as join9, resolve as resolve2 } from "path";
4398
- import debugLib9 from "debug";
4399
- 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");
4400
4326
  function mountdiskImgCacheDir() {
4401
4327
  return join9(homedir6(), ".cache", "machinen", "mountdisk");
4402
4328
  }
@@ -4404,7 +4330,7 @@ function okMarkerPath2(imgPath) {
4404
4330
  return `${imgPath}.ok`;
4405
4331
  }
4406
4332
  function markMountDiskImageClean(imgPath) {
4407
- if (!existsSync10(imgPath)) {
4333
+ if (!existsSync9(imgPath)) {
4408
4334
  return;
4409
4335
  }
4410
4336
  const okPath = okMarkerPath2(imgPath);
@@ -4417,7 +4343,7 @@ function markMountDiskImageClean(imgPath) {
4417
4343
  fd = -1;
4418
4344
  renameSync4(tmp, okPath);
4419
4345
  } catch (err) {
4420
- debug9("markMountDiskImageClean failed img=%s err=%s", imgPath, err.message);
4346
+ debug8("markMountDiskImageClean failed img=%s err=%s", imgPath, err.message);
4421
4347
  if (fd >= 0) {
4422
4348
  try {
4423
4349
  closeSync3(fd);
@@ -4432,7 +4358,7 @@ function markMountDiskImageClean(imgPath) {
4432
4358
  }
4433
4359
  function ensureMountDiskImage(hostAbs, opts = {}) {
4434
4360
  const hostResolved = resolve2(hostAbs);
4435
- if (!existsSync10(hostResolved)) {
4361
+ if (!existsSync9(hostResolved)) {
4436
4362
  throw new BootError(
4437
4363
  "BOOT_MOUNT_HOST_NOT_FOUND",
4438
4364
  `ensureMountDiskImage: host directory not found at ${hostResolved}`
@@ -4451,10 +4377,10 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4451
4377
  opts.onPhase?.("manifest-hash", Date.now() - hashT0);
4452
4378
  const imgPath = join9(cacheDir, `${key}.sqfs`);
4453
4379
  const okPath = okMarkerPath2(imgPath);
4454
- if (!opts.force && existsSync10(imgPath)) {
4455
- debug9("cache hit key=%s img=%s", key.slice(0, 12), imgPath);
4456
- if (!existsSync10(okPath)) {
4457
- 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);
4458
4384
  } else {
4459
4385
  try {
4460
4386
  unlinkSync6(okPath);
@@ -4473,7 +4399,7 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4473
4399
  const stagingDir = mkdtempSync4(join9(cacheDir, `${key.slice(0, 12)}-staging-`));
4474
4400
  const stagingImg = join9(stagingDir, "lower.sqfs");
4475
4401
  try {
4476
- debug9("materialize key=%s host=%s", key.slice(0, 12), hostResolved);
4402
+ debug8("materialize key=%s host=%s", key.slice(0, 12), hostResolved);
4477
4403
  const mkT0 = Date.now();
4478
4404
  const args = [
4479
4405
  hostResolved,
@@ -4510,11 +4436,11 @@ function ensureMountDiskImage(hostAbs, opts = {}) {
4510
4436
  padTo512Boundary(stagingImg);
4511
4437
  renameSync4(stagingImg, imgPath);
4512
4438
  opts.onPhase?.("staging-rename", Date.now() - renameT0);
4513
- 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);
4514
4440
  return { lowerPath: imgPath, key };
4515
4441
  } finally {
4516
4442
  try {
4517
- rmSync7(stagingDir, { recursive: true, force: true });
4443
+ rmSync6(stagingDir, { recursive: true, force: true });
4518
4444
  } catch {
4519
4445
  }
4520
4446
  }
@@ -4560,8 +4486,8 @@ function resolveMksquashfsEnvOverride() {
4560
4486
  if (!envOverride) {
4561
4487
  return void 0;
4562
4488
  }
4563
- if (existsSync10(envOverride)) {
4564
- debug9("resolved via MACHINEN_MKSQUASHFS=%s", envOverride);
4489
+ if (existsSync9(envOverride)) {
4490
+ debug8("resolved via MACHINEN_MKSQUASHFS=%s", envOverride);
4565
4491
  return envOverride;
4566
4492
  }
4567
4493
  throw new BootError(
@@ -4571,12 +4497,11 @@ function resolveMksquashfsEnvOverride() {
4571
4497
  }
4572
4498
  var require_4 = createRequire4(import.meta.url);
4573
4499
  function findBundledMksquashfs() {
4574
- const pkg = `@machinen/squashfs-tools-${arch2()}-${platform4()}`;
4500
+ const pkg = `@machinen/native-${arch2()}-${platform4()}`;
4575
4501
  try {
4576
- const pkgJson = require_4.resolve(`${pkg}/package.json`);
4577
- const candidate = join9(dirname8(pkgJson), "bin", "mksquashfs");
4578
- if (existsSync10(candidate)) {
4579
- return candidate;
4502
+ const mod = require_4(pkg);
4503
+ if (mod.mksquashfs && existsSync9(mod.mksquashfs)) {
4504
+ return mod.mksquashfs;
4580
4505
  }
4581
4506
  } catch {
4582
4507
  }
@@ -4593,7 +4518,7 @@ var KEG_ONLY_DIRS = [
4593
4518
  function findKegOnlyMksquashfs() {
4594
4519
  for (const dir of KEG_ONLY_DIRS) {
4595
4520
  const candidate = join9(dir, "mksquashfs");
4596
- if (existsSync10(candidate)) {
4521
+ if (existsSync9(candidate)) {
4597
4522
  return candidate;
4598
4523
  }
4599
4524
  }
@@ -4703,12 +4628,12 @@ function randomSuffix() {
4703
4628
 
4704
4629
  // src/vm/helpers.ts
4705
4630
  import { randomBytes } from "crypto";
4706
- 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";
4707
4632
  import { createRequire as createRequire5 } from "module";
4708
4633
  import { arch as osArch3, platform as osPlatform6, totalmem as totalmem2 } from "os";
4709
4634
  import { resolve as resolve3 } from "path";
4710
- import debugLib10 from "debug";
4711
- var debug10 = debugLib10("machinen:boot");
4635
+ import debugLib9 from "debug";
4636
+ var debug9 = debugLib9("machinen:boot");
4712
4637
  var require_5 = createRequire5(import.meta.url);
4713
4638
  var SNAP_SCRATCH_BYTES = 8 * 1024 * 1024 * 1024;
4714
4639
  function allocateSparseFile3(path, sizeBytes) {
@@ -4746,7 +4671,7 @@ function resolveVmmBinary() {
4746
4671
  const envOverride = process.env.MACHINEN_VMM;
4747
4672
  if (envOverride) {
4748
4673
  const abs = resolve3(envOverride);
4749
- if (!existsSync11(abs)) {
4674
+ if (!existsSync10(abs)) {
4750
4675
  throw new BootError(
4751
4676
  "BOOT_VMM_MISSING",
4752
4677
  `MACHINEN_VMM is set to ${envOverride}, but that file does not exist.`
@@ -4755,13 +4680,13 @@ function resolveVmmBinary() {
4755
4680
  return abs;
4756
4681
  }
4757
4682
  const key = `${osArch3()}-${osPlatform6()}`;
4758
- const pkgName = `@machinen/vmm-${key}`;
4683
+ const pkgName = `@machinen/native-${key}`;
4759
4684
  try {
4760
4685
  const mod = require_5(pkgName);
4761
- if (!mod.binary || !existsSync11(mod.binary)) {
4686
+ if (!mod.binary || !existsSync10(mod.binary)) {
4762
4687
  throw new BootError(
4763
4688
  "BOOT_VMM_PACKAGE_BROKEN",
4764
- `${pkgName} is installed but its binary is missing at ${mod.binary}.`
4689
+ `${pkgName} is installed but its VMM binary is missing at ${mod.binary}.`
4765
4690
  );
4766
4691
  }
4767
4692
  return mod.binary;
@@ -4917,7 +4842,7 @@ function buildGuestHostname(pid, name) {
4917
4842
  }
4918
4843
  async function setGuestHostname(vm, hostname) {
4919
4844
  if (hostname.length === 0 || hostname.includes("'") || hostname.includes("\n") || hostname.includes("\0")) {
4920
- debug10("setGuestHostname: refusing unsafe hostname %j", hostname);
4845
+ debug9("setGuestHostname: refusing unsafe hostname %j", hostname);
4921
4846
  return;
4922
4847
  }
4923
4848
  try {
@@ -4925,9 +4850,9 @@ async function setGuestHostname(vm, hostname) {
4925
4850
  connectTimeoutMs: 5e3,
4926
4851
  execTimeoutMs: 5e3
4927
4852
  });
4928
- debug10("setGuestHostname: set pid=%d hostname=%s", vm.pid, hostname);
4853
+ debug9("setGuestHostname: set pid=%d hostname=%s", vm.pid, hostname);
4929
4854
  } catch (err) {
4930
- debug10(
4855
+ debug9(
4931
4856
  "setGuestHostname: failed pid=%d hostname=%s err=%s",
4932
4857
  vm.pid,
4933
4858
  hostname,
@@ -4938,7 +4863,7 @@ async function setGuestHostname(vm, hostname) {
4938
4863
 
4939
4864
  // src/vm/image-config.ts
4940
4865
  import { execFileSync as execFileSync7 } from "child_process";
4941
- 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";
4942
4867
  import { homedir as homedir7 } from "os";
4943
4868
  import { join as join10 } from "path";
4944
4869
  function imageConfigCacheDir() {
@@ -4968,9 +4893,9 @@ function warmImageConfigCache(imagePath, config) {
4968
4893
  }
4969
4894
  function readImageConfig(imagePath) {
4970
4895
  const cachePath = imageConfigCachePath(imagePath);
4971
- if (cachePath && existsSync12(cachePath)) {
4896
+ if (cachePath && existsSync11(cachePath)) {
4972
4897
  try {
4973
- const raw = readFileSync9(cachePath, "utf8");
4898
+ const raw = readFileSync8(cachePath, "utf8");
4974
4899
  const cached = JSON.parse(raw);
4975
4900
  return cached.config ?? void 0;
4976
4901
  } catch {
@@ -4998,12 +4923,18 @@ function readImageConfig(imagePath) {
4998
4923
  }
4999
4924
 
5000
4925
  // src/vm/bundle.ts
5001
- var LIVE_MOUNT_PORT_BASE = 1970;
5002
- 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
+ }
5003
4934
  return mounts.map((m, i) => {
5004
4935
  validateMountGuest(m.guest);
5005
4936
  const hostAbs = resolve4(cwd ?? process.cwd(), m.host);
5006
- if (!existsSync13(hostAbs)) {
4937
+ if (!existsSync12(hostAbs)) {
5007
4938
  throw new BootError(
5008
4939
  "BOOT_MOUNT_HOST_NOT_FOUND",
5009
4940
  `liveMounts[${i}] host path not found: ${m.host}`
@@ -5018,10 +4949,10 @@ function resolveLiveMounts(mounts, cwd, udsDir) {
5018
4949
  return {
5019
4950
  host: hostAbs,
5020
4951
  guest: normalizeMountGuest(m.guest),
5021
- port: LIVE_MOUNT_PORT_BASE + i,
5022
- udsPath: join11(udsDir, `live-mount-${i}.sock`),
5023
- statsPath: join11(udsDir, `live-mount-${i}-stats.json`),
5024
- 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}`
5025
4956
  };
5026
4957
  });
5027
4958
  }
@@ -5032,7 +4963,7 @@ function buildMachinenConfig(input) {
5032
4963
  cfg.cwd = effectiveCwd;
5033
4964
  }
5034
4965
  if (input.liveMounts.length > 0) {
5035
- cfg.liveMounts = input.liveMounts.map(({ guest, port }) => ({ guest, port }));
4966
+ cfg.liveMounts = input.liveMounts.map((lm) => ({ guest: lm.guest, tag: lm.tag }));
5036
4967
  }
5037
4968
  return cfg;
5038
4969
  }
@@ -5070,7 +5001,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5070
5001
  const synthBundleDir = join11(tempDir, "bundle");
5071
5002
  const cleanup = () => {
5072
5003
  try {
5073
- rmSync8(tempDir, { recursive: true, force: true });
5004
+ rmSync7(tempDir, { recursive: true, force: true });
5074
5005
  } catch {
5075
5006
  }
5076
5007
  };
@@ -5086,7 +5017,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5086
5017
  let imageConfig;
5087
5018
  if (opts.image) {
5088
5019
  baseAbs = resolve4(opts.cwd ?? process.cwd(), opts.image);
5089
- if (!existsSync13(baseAbs)) {
5020
+ if (!existsSync12(baseAbs)) {
5090
5021
  cleanup();
5091
5022
  throw new BootError("BOOT_IMAGE_NOT_FOUND", `image tarball not found: ${baseAbs}`);
5092
5023
  }
@@ -5099,6 +5030,8 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5099
5030
  effectiveCmd = opts.cmd;
5100
5031
  } else if (typeof opts.snapshot === "string") {
5101
5032
  effectiveCmd = ["/sbin/machinen-restore"];
5033
+ } else if (opts._vmstateRestorePath) {
5034
+ effectiveCmd = ["/sbin/machinen-poweroff"];
5102
5035
  } else if (imageConfig?.cmd) {
5103
5036
  effectiveCmd = imageConfig.cmd;
5104
5037
  }
@@ -5133,7 +5066,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5133
5066
  throw err;
5134
5067
  }
5135
5068
  const hostAbs = resolve4(opts.cwd ?? process.cwd(), opts.mount.host);
5136
- if (!existsSync13(hostAbs)) {
5069
+ if (!existsSync12(hostAbs)) {
5137
5070
  cleanup();
5138
5071
  throw new BootError(
5139
5072
  "BOOT_MOUNT_HOST_NOT_FOUND",
@@ -5160,8 +5093,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5160
5093
  // sourced from a snapshot bundle) rides on virtio-blk slots
5161
5094
  // 5+6, attached further down in boot().
5162
5095
  mountGuest: mount?.guest ?? opts._restoreMountDisk?.guest,
5163
- env: effectiveEnv,
5164
- fuseAgentPath: liveMounts.length > 0 ? defaultFuseAgentPath() : void 0
5096
+ env: effectiveEnv
5165
5097
  });
5166
5098
  } else {
5167
5099
  packBundle({
@@ -5169,8 +5101,7 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5169
5101
  out: cpioPath,
5170
5102
  base: baseAbs,
5171
5103
  mount,
5172
- env: effectiveEnv,
5173
- fuseAgentPath: liveMounts.length > 0 ? defaultFuseAgentPath() : void 0
5104
+ env: effectiveEnv
5174
5105
  });
5175
5106
  }
5176
5107
  packerOpts.onPhase?.("cpio-write", Date.now() - packT0);
@@ -5184,13 +5115,13 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5184
5115
  if (opts._restoreMountDisk) {
5185
5116
  try {
5186
5117
  const r = opts._restoreMountDisk;
5187
- if (!existsSync13(r.lowerPath)) {
5118
+ if (!existsSync12(r.lowerPath)) {
5188
5119
  throw new BootError(
5189
5120
  "BOOT_SNAPSHOT_NOT_FOUND",
5190
5121
  `restore: bundle is missing mount-lower at ${r.lowerPath}`
5191
5122
  );
5192
5123
  }
5193
- if (!existsSync13(r.upperPath)) {
5124
+ if (!existsSync12(r.upperPath)) {
5194
5125
  throw new BootError(
5195
5126
  "BOOT_SNAPSHOT_NOT_FOUND",
5196
5127
  `restore: bundle is missing mount-upper at ${r.upperPath}`
@@ -5237,12 +5168,45 @@ function synthesizeAndPackBundle(opts, mergedGuestEnv, liveMounts, packerOpts) {
5237
5168
  }
5238
5169
 
5239
5170
  // src/vm/snapshot.ts
5240
- import { spawn as nodeSpawn3 } from "child_process";
5241
- 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";
5242
5181
  import { join as join12, resolve as resolve5 } from "path";
5243
- import debugLib11 from "debug";
5244
- 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");
5245
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) {
5246
5210
  const baseDumpCmd = opts.dumpCmd ?? "/sbin/machinen-dump";
5247
5211
  const deadlineMs = opts.timeoutMs ?? 9e4;
5248
5212
  const onLog = opts.onLog;
@@ -5284,7 +5248,6 @@ async function performSnapshot(ctx, opts) {
5284
5248
  const imgEntries = readdirSync6(imgDir);
5285
5249
  validateDumpResult(dumpRes, imgEntries, consoleLog, deadlineMs);
5286
5250
  const mountDiskMeta = await reflinkMountOverlay(ctx, snapDir, deadlineMs);
5287
- await manageLiveMountServers(ctx, leaveRunning, deadlineMs, onLog);
5288
5251
  if (!leaveRunning) {
5289
5252
  phases.start("poweroff");
5290
5253
  await powerOffSourceVm(ctx, deadlineMs);
@@ -5292,15 +5255,15 @@ async function performSnapshot(ctx, opts) {
5292
5255
  }
5293
5256
  phases.end("validation");
5294
5257
  phases.start("finalize");
5295
- writeSnapshotMeta(ctx, snapDir, mountDiskMeta);
5258
+ writeSnapshotMeta(ctx, snapDir, mountDiskMeta, "criu");
5296
5259
  phases.end("finalize");
5297
5260
  debugSnapshot("snapshot done snapDir=%s imgEntries=%d", snapDir, imgEntries.length);
5298
5261
  phases.flush(debugSnapshot, "snapshot");
5299
- return { snapDir, imgDir, elapsedMs, consoleLog };
5262
+ return { engine: "criu", snapDir, imgDir, elapsedMs, consoleLog };
5300
5263
  }
5301
- function prepareBundleDir(outDir) {
5264
+ function prepareBundleRootDir(outDir) {
5302
5265
  const snapDir = resolve5(outDir);
5303
- if (existsSync14(snapDir)) {
5266
+ if (existsSync13(snapDir)) {
5304
5267
  if (!statSync9(snapDir).isDirectory()) {
5305
5268
  throw new SnapshotError(
5306
5269
  "SNAPSHOT_DUMP_FAILED",
@@ -5316,12 +5279,16 @@ function prepareBundleDir(outDir) {
5316
5279
  } else {
5317
5280
  mkdirSync11(snapDir, { recursive: true });
5318
5281
  }
5282
+ return snapDir;
5283
+ }
5284
+ function prepareBundleDir(outDir) {
5285
+ const snapDir = prepareBundleRootDir(outDir);
5319
5286
  const imgDir = join12(snapDir, "img");
5320
5287
  mkdirSync11(imgDir, { recursive: true });
5321
5288
  return { snapDir, imgDir };
5322
5289
  }
5323
5290
  async function runDumpAndExtractTar(ctx, dumpCmd, deadlineMs, onLog, imgDir) {
5324
- const tarChild = nodeSpawn3("tar", ["-xmf", "-", "-C", imgDir], {
5291
+ const tarChild = nodeSpawn2("tar", ["-xmf", "-", "-C", imgDir], {
5325
5292
  stdio: ["pipe", "ignore", "pipe"]
5326
5293
  });
5327
5294
  let tarStderr = "";
@@ -5467,48 +5434,6 @@ async function reflinkMountOverlay(ctx, snapDir, deadlineMs) {
5467
5434
  upper: upperName
5468
5435
  };
5469
5436
  }
5470
- async function manageLiveMountServers(ctx, leaveRunning, deadlineMs, onLog) {
5471
- if (!ctx.liveMounts || ctx.liveMounts.length === 0) {
5472
- return;
5473
- }
5474
- if (ctx.stopLiveMountServers) {
5475
- try {
5476
- await ctx.stopLiveMountServers();
5477
- } catch (err) {
5478
- debugSnapshot(
5479
- "stopLiveMountServers failed (continuing): %s",
5480
- err instanceof Error ? err.message : String(err)
5481
- );
5482
- }
5483
- }
5484
- if (!leaveRunning) {
5485
- return;
5486
- }
5487
- if (ctx.respawnLiveMountServers) {
5488
- try {
5489
- await ctx.respawnLiveMountServers();
5490
- } catch (err) {
5491
- throw new SnapshotError(
5492
- "SNAPSHOT_DUMP_FAILED",
5493
- `vm.snapshot: failed to respawn live-mount servers post-dump: ${err instanceof Error ? err.message : String(err)}`,
5494
- { cause: err }
5495
- );
5496
- }
5497
- }
5498
- try {
5499
- await ctx.execRaw("/sbin/machinen-remount", {
5500
- connectTimeoutMs: Math.min(deadlineMs, 5e3),
5501
- execTimeoutMs: 15e3,
5502
- onStderr: onLog ? (chunk) => onLog({ source: "exec-stderr", cmd: "/sbin/machinen-remount", chunk }) : void 0
5503
- });
5504
- } catch (err) {
5505
- throw new SnapshotError(
5506
- "SNAPSHOT_DUMP_FAILED",
5507
- `vm.snapshot: post-dump remount in guest failed: ${err instanceof Error ? err.message : String(err)}`,
5508
- { cause: err }
5509
- );
5510
- }
5511
- }
5512
5437
  async function powerOffSourceVm(ctx, deadlineMs) {
5513
5438
  debugSnapshot("issuing /sbin/machinen-poweroff to bring VMM down");
5514
5439
  ctx.execRaw("/sbin/machinen-poweroff", {
@@ -5531,8 +5456,9 @@ async function powerOffSourceVm(ctx, deadlineMs) {
5531
5456
  clearTimeout(powerOffTimer);
5532
5457
  }
5533
5458
  }
5534
- function writeSnapshotMeta(ctx, snapDir, mountDiskMeta) {
5459
+ function writeSnapshotMeta(ctx, snapDir, mountDiskMeta, engine) {
5535
5460
  const meta = {
5461
+ engine,
5536
5462
  sourceName: ctx.sourceName,
5537
5463
  sourceImage: ctx.sourceImage,
5538
5464
  snappedAt: Date.now(),
@@ -5541,7 +5467,11 @@ function writeSnapshotMeta(ctx, snapDir, mountDiskMeta) {
5541
5467
  // snapshotting host that the restoring host has to resolve (or
5542
5468
  // remap via `restore({ liveMounts })`). Recorded only when ctx
5543
5469
  // carried a resolved list (boot-handle path).
5544
- 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
5545
5475
  };
5546
5476
  writeFileSync8(join12(snapDir, "meta.json"), JSON.stringify(meta));
5547
5477
  }
@@ -5561,14 +5491,107 @@ Dump exec exited ${outcome.exitCode} (workload kept running).`;
5561
5491
  }
5562
5492
  return "";
5563
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
+ }
5564
5585
 
5565
5586
  // src/vm/boot.ts
5566
- var debug11 = debugLib12("machinen:boot");
5567
- 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");
5568
5591
  async function boot(opts = {}) {
5569
5592
  const bootT0 = Date.now();
5570
5593
  const phases = new PhaseTimer();
5571
- debug11(
5594
+ debug10(
5572
5595
  "boot entry image=%s cmd=%j name=%s portForward=%d hasSnapshot=%s mount=%s",
5573
5596
  opts.image ?? "<none>",
5574
5597
  opts.cmd ?? null,
@@ -5582,7 +5605,7 @@ async function boot(opts = {}) {
5582
5605
  await validatePortForwardOpts(opts, portForward);
5583
5606
  const binaryInput = opts.binary ?? resolveVmmBinary();
5584
5607
  const binary = resolve6(opts.cwd ?? process.cwd(), binaryInput);
5585
- if (!existsSync15(binary)) {
5608
+ if (!existsSync14(binary)) {
5586
5609
  throw new BootError("BOOT_VMM_MISSING", `VMM binary not found at ${binary}`);
5587
5610
  }
5588
5611
  if (opts.cmd && !opts.image) {
@@ -5607,21 +5630,31 @@ async function boot(opts = {}) {
5607
5630
  validateKernelDtb(opts, env);
5608
5631
  let { vsockUdsPath, vsockTempDir } = setupVsockBridge(env);
5609
5632
  const { statsFilePath, statsTempDir } = setupStatsFile(env, vsockTempDir);
5610
- let liveMountsResolved = [];
5611
- if ((opts.liveMounts ?? []).length > 0) {
5633
+ let vmstateStatePath;
5634
+ if (resolveSnapshotEngine() === "vmstate" && opts.snapshot !== false) {
5612
5635
  if (!vsockTempDir) {
5613
5636
  vsockTempDir = mkdtempSync6(join13(tmpdir5(), "machinen-vsock-"));
5614
5637
  }
5615
- liveMountsResolved = resolveLiveMounts(opts.liveMounts, opts.cwd, vsockTempDir);
5616
- for (const lm of liveMountsResolved) {
5617
- 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";
5618
5645
  }
5619
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
+ }
5620
5654
  let gvStop;
5621
5655
  let gvPid;
5622
5656
  let gvExe;
5623
5657
  let gvSocketDir;
5624
- const liveMountServers = [];
5625
5658
  let bundleTempDir;
5626
5659
  let mountDiskPaths;
5627
5660
  let perBootMountUpper;
@@ -5630,6 +5663,9 @@ async function boot(opts = {}) {
5630
5663
  if (opts.name && !mergedGuestEnv.MACHINEN_VM_NAME) {
5631
5664
  mergedGuestEnv.MACHINEN_VM_NAME = opts.name;
5632
5665
  }
5666
+ if (vsockUdsPath && !mergedGuestEnv.MACHINEN_VM_HOSTNAME_WAIT) {
5667
+ mergedGuestEnv.MACHINEN_VM_HOSTNAME_WAIT = "1";
5668
+ }
5633
5669
  try {
5634
5670
  phases.start("net-services");
5635
5671
  phases.start("net-services.gvproxy");
@@ -5652,7 +5688,7 @@ async function boot(opts = {}) {
5652
5688
  env.MACHINEN_INITRD = packed.cpioPath;
5653
5689
  mountDiskPaths = packed.mountDisk;
5654
5690
  const packMs = phases.end("initramfs-pack");
5655
- debug11("initramfs packed cpio=%s elapsed=%dms", packed.cpioPath, packMs ?? -1);
5691
+ debug10("initramfs packed cpio=%s elapsed=%dms", packed.cpioPath, packMs ?? -1);
5656
5692
  }
5657
5693
  phases.start("rootdisk-materialize");
5658
5694
  if (wantsRootDisk) {
@@ -5678,7 +5714,7 @@ async function boot(opts = {}) {
5678
5714
  if (mountDiskPaths) {
5679
5715
  perBootMountUpper = mountDiskPaths.upperPath;
5680
5716
  }
5681
- const child = nodeSpawn4(wrappedVmm.command, wrappedVmm.args, {
5717
+ const child = nodeSpawn3(wrappedVmm.command, wrappedVmm.args, {
5682
5718
  cwd: opts.cwd,
5683
5719
  env,
5684
5720
  stdio
@@ -5688,7 +5724,7 @@ async function boot(opts = {}) {
5688
5724
  }
5689
5725
  phases.end("vmm-spawn");
5690
5726
  phases.start("first-guest-byte");
5691
- debug11(
5727
+ debug10(
5692
5728
  "VMM spawned pid=%d binary=%s wrapped=%s elapsedSinceEntry=%dms",
5693
5729
  child.pid ?? -1,
5694
5730
  binary,
@@ -5728,7 +5764,8 @@ async function boot(opts = {}) {
5728
5764
  memoryCeilingMib,
5729
5765
  statsFilePath,
5730
5766
  mountDiskPaths,
5731
- liveMountsResolved
5767
+ liveMountsResolved,
5768
+ vmstateStatePath
5732
5769
  }) : false;
5733
5770
  installVmExitCleanup({
5734
5771
  child,
@@ -5741,7 +5778,6 @@ async function boot(opts = {}) {
5741
5778
  vsockTempDir,
5742
5779
  statsTempDir,
5743
5780
  gvStop,
5744
- liveMountServers,
5745
5781
  registered
5746
5782
  });
5747
5783
  const timeoutMs = opts.timeoutMs === void 0 ? 6e4 : opts.timeoutMs;
@@ -5758,22 +5794,12 @@ async function boot(opts = {}) {
5758
5794
  process.stderr.write(chunk);
5759
5795
  });
5760
5796
  }
5797
+ installVmstateTimingRelay(child);
5761
5798
  installFlushPhases(child, phases, onLog);
5762
5799
  const detachedBootChunks = [];
5763
5800
  if (opts.detached) {
5764
5801
  installDetachedBootCapture(child, detachedBootChunks);
5765
5802
  }
5766
- if (liveMountsResolved.length > 0) {
5767
- phases.start("net-services.live-mounts");
5768
- await spawnLiveMountServersForBoot({
5769
- liveMountsResolved,
5770
- childPid,
5771
- child,
5772
- registered,
5773
- liveMountServers
5774
- });
5775
- phases.end("net-services.live-mounts");
5776
- }
5777
5803
  const handle = {
5778
5804
  pid: childPid,
5779
5805
  name: vmName,
@@ -5842,20 +5868,16 @@ ${res.stderr}`
5842
5868
  const balloon = statsFilePath ? readBalloonStats(statsFilePath) : null;
5843
5869
  const cur = findEntry({ pid: childPid });
5844
5870
  const lazyTotal = cur?.lazyPagesTotal ?? 0;
5845
- let bytesServed = 0;
5846
- for (const server of liveMountServers) {
5847
- bytesServed += server.bytesServedOnPagesImg();
5848
- }
5849
- const pagesServed = Math.floor(bytesServed / 4096);
5850
5871
  return {
5851
5872
  ceilingMib: memoryCeilingMib ?? null,
5852
5873
  hostRssBytes: readHostRssBytes(childPid, statsFilePath),
5853
5874
  balloonInflatedBytes: balloon?.bytesReported ?? 0,
5854
- lazyPagesPending: Math.max(0, lazyTotal - pagesServed)
5875
+ lazyPagesPending: lazyTotal
5855
5876
  };
5856
5877
  },
5857
5878
  async snapshot(snapshotOpts) {
5858
- if (!diskAbs) {
5879
+ const engine = resolveSnapshotEngine();
5880
+ if (engine === "criu" && !diskAbs || engine === "vmstate" && !vmstateStatePath) {
5859
5881
  throw new SnapshotError(
5860
5882
  "SNAPSHOT_NO_DISK",
5861
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>'`."
@@ -5864,7 +5886,8 @@ ${res.stderr}`
5864
5886
  return performSnapshot(buildBootSnapshotContext(), snapshotOpts);
5865
5887
  },
5866
5888
  async fork(forkOpts) {
5867
- if (!diskAbs) {
5889
+ const engine = resolveSnapshotEngine();
5890
+ if (engine === "criu" && !diskAbs || engine === "vmstate" && !vmstateStatePath) {
5868
5891
  throw new SnapshotError(
5869
5892
  "SNAPSHOT_NO_DISK",
5870
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."
@@ -5890,23 +5913,7 @@ ${res.stderr}`
5890
5913
  upperPath: mountDiskPaths.upperPath
5891
5914
  } : void 0,
5892
5915
  liveMounts: liveMountsForCtx,
5893
- stopLiveMountServers: liveMountsForCtx ? async () => {
5894
- await Promise.all(liveMountServers.map((s) => s.stop().catch(() => {
5895
- })));
5896
- liveMountServers.length = 0;
5897
- } : void 0,
5898
- respawnLiveMountServers: liveMountsForCtx ? async () => {
5899
- for (const lm of liveMountsResolved) {
5900
- const fresh = await spawnDetachedMountServer({
5901
- udsPath: lm.udsPath,
5902
- rootAbs: lm.host,
5903
- mode: lm.mode,
5904
- vmmPid: childPid,
5905
- statsPath: lm.statsPath
5906
- });
5907
- liveMountServers.push(fresh);
5908
- }
5909
- } : void 0,
5916
+ vmstatePath: vmstateStatePath,
5910
5917
  execRaw: (cmd, execOpts) => handle.execRaw(cmd, execOpts),
5911
5918
  wait: () => handle.wait(),
5912
5919
  kill: () => handle.kill(),
@@ -5990,12 +5997,12 @@ function prepareScratchDisk(opts, env) {
5990
5997
  }
5991
5998
  if (typeof opts.snapshot === "string") {
5992
5999
  const bundleDisk = resolve6(opts.cwd ?? process.cwd(), opts.snapshot);
5993
- if (!existsSync15(bundleDisk)) {
6000
+ if (!existsSync14(bundleDisk)) {
5994
6001
  throw new BootError("BOOT_SNAPSHOT_NOT_FOUND", `snapshot image not found: ${bundleDisk}`);
5995
6002
  }
5996
6003
  if (opts.cmd) {
5997
6004
  env.MACHINEN_DISK = bundleDisk;
5998
- debug11("snap-restore in-place (explicit cmd) path=%s", bundleDisk);
6005
+ debug10("snap-restore in-place (explicit cmd) path=%s", bundleDisk);
5999
6006
  return { diskAbs: bundleDisk, perBootSnapDisk: void 0 };
6000
6007
  }
6001
6008
  const perBoot = join13(
@@ -6004,7 +6011,7 @@ function prepareScratchDisk(opts, env) {
6004
6011
  );
6005
6012
  reflinkCopy(bundleDisk, perBoot);
6006
6013
  env.MACHINEN_DISK = perBoot;
6007
- debug11("snap-restore reflink-clone src=%s dst=%s", bundleDisk, perBoot);
6014
+ debug10("snap-restore reflink-clone src=%s dst=%s", bundleDisk, perBoot);
6008
6015
  return { diskAbs: perBoot, perBootSnapDisk: perBoot };
6009
6016
  }
6010
6017
  if (!opts.image) {
@@ -6016,20 +6023,20 @@ function prepareScratchDisk(opts, env) {
6016
6023
  );
6017
6024
  allocateSparseFile3(scratchPath, SNAP_SCRATCH_BYTES);
6018
6025
  env.MACHINEN_DISK = scratchPath;
6019
- 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);
6020
6027
  return { diskAbs: scratchPath, perBootSnapDisk: scratchPath };
6021
6028
  }
6022
6029
  function validateKernelDtb(opts, env) {
6023
6030
  if (opts.kernel) {
6024
6031
  const abs = resolve6(opts.cwd ?? process.cwd(), opts.kernel);
6025
- if (!existsSync15(abs)) {
6032
+ if (!existsSync14(abs)) {
6026
6033
  throw new BootError("BOOT_KERNEL_NOT_FOUND", `kernel not found: ${abs}`);
6027
6034
  }
6028
6035
  env.MACHINEN_KERNEL = abs;
6029
6036
  }
6030
6037
  if (opts.dtb) {
6031
6038
  const abs = resolve6(opts.cwd ?? process.cwd(), opts.dtb);
6032
- if (!existsSync15(abs)) {
6039
+ if (!existsSync14(abs)) {
6033
6040
  throw new BootError("BOOT_DTB_NOT_FOUND", `dtb not found: ${abs}`);
6034
6041
  }
6035
6042
  env.MACHINEN_DTB = abs;
@@ -6038,7 +6045,7 @@ function validateKernelDtb(opts, env) {
6038
6045
  function setupVsockBridge(env) {
6039
6046
  if (env.MACHINEN_VSOCK) {
6040
6047
  const vsockUdsPath2 = parseVsockUdsPath(env.MACHINEN_VSOCK);
6041
- debug11(
6048
+ debug10(
6042
6049
  "vsock spec from caller env: %s (uds=%s)",
6043
6050
  env.MACHINEN_VSOCK,
6044
6051
  vsockUdsPath2 ?? "<unparsed>"
@@ -6048,7 +6055,7 @@ function setupVsockBridge(env) {
6048
6055
  const vsockTempDir = mkdtempSync6(join13(tmpdir5(), "machinen-vsock-"));
6049
6056
  const vsockUdsPath = join13(vsockTempDir, "exec.sock");
6050
6057
  env.MACHINEN_VSOCK = `in:1978:${vsockUdsPath}`;
6051
- debug11("vsock auto uds=%s", vsockUdsPath);
6058
+ debug10("vsock auto uds=%s", vsockUdsPath);
6052
6059
  return { vsockUdsPath, vsockTempDir };
6053
6060
  }
6054
6061
  function setupStatsFile(env, vsockTempDir) {
@@ -6074,7 +6081,7 @@ function setupStatsFile(env, vsockTempDir) {
6074
6081
  }
6075
6082
  async function bringUpGvproxy(opts, binary, env, portForward) {
6076
6083
  if (env.MACHINEN_NET_SOCKET) {
6077
- debug11("MACHINEN_NET_SOCKET already set \u2014 skipping gvproxy spawn");
6084
+ debug10("MACHINEN_NET_SOCKET already set \u2014 skipping gvproxy spawn");
6078
6085
  return { gvStop: void 0, gvPid: void 0, gvExe: void 0, gvSocketDir: void 0 };
6079
6086
  }
6080
6087
  const gvBin = await ensureGvproxy(binary);
@@ -6085,11 +6092,11 @@ async function bringUpGvproxy(opts, binary, env, portForward) {
6085
6092
  "portForward requires gvproxy, but no gvproxy binary was found. Install gvproxy or point MACHINEN_GVPROXY at one."
6086
6093
  );
6087
6094
  }
6088
- debug11("gvproxy not found \u2014 booting without networking");
6095
+ debug10("gvproxy not found \u2014 booting without networking");
6089
6096
  warnGvproxyMissing();
6090
6097
  return { gvStop: void 0, gvPid: void 0, gvExe: void 0, gvSocketDir: void 0 };
6091
6098
  }
6092
- debug11("starting gvproxy bin=%s", gvBin);
6099
+ debug10("starting gvproxy bin=%s", gvBin);
6093
6100
  const gv = await spawnGvproxy(gvBin, { detached: opts.detached });
6094
6101
  env.MACHINEN_NET_SOCKET = gv.socketPath;
6095
6102
  for (const m of portForward) {
@@ -6105,7 +6112,7 @@ async function bringUpGvproxy(opts, binary, env, portForward) {
6105
6112
  function materializeRootdisk(opts, env, phases) {
6106
6113
  if (typeof opts.rootDisk === "string") {
6107
6114
  const rootDiskAbs = resolve6(opts.cwd ?? process.cwd(), opts.rootDisk);
6108
- if (!existsSync15(rootDiskAbs)) {
6115
+ if (!existsSync14(rootDiskAbs)) {
6109
6116
  throw new BootError("BOOT_IMAGE_NOT_FOUND", `rootDisk image not found: ${rootDiskAbs}`);
6110
6117
  }
6111
6118
  env.MACHINEN_ROOTDISK = rootDiskAbs;
@@ -6137,7 +6144,7 @@ function rollbackPreSpawn(state) {
6137
6144
  for (const dir of [state.bundleTempDir, state.vsockTempDir]) {
6138
6145
  if (dir) {
6139
6146
  try {
6140
- rmSync9(dir, { recursive: true, force: true });
6147
+ rmSync8(dir, { recursive: true, force: true });
6141
6148
  } catch {
6142
6149
  }
6143
6150
  }
@@ -6145,7 +6152,7 @@ function rollbackPreSpawn(state) {
6145
6152
  for (const file of [state.perBootRootDisk, state.perBootSnapDisk, state.perBootMountUpper]) {
6146
6153
  if (file) {
6147
6154
  try {
6148
- unlinkSync7(file);
6155
+ unlinkSync8(file);
6149
6156
  } catch {
6150
6157
  }
6151
6158
  }
@@ -6255,6 +6262,10 @@ function registerInRegistry(args) {
6255
6262
  portForward: args.portForward.length > 0 ? args.portForward : void 0,
6256
6263
  memoryCeilingMib: args.memoryCeilingMib,
6257
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,
6258
6269
  // #272: persist mount-overlay paths so an attach-owned
6259
6270
  // vm.snapshot()/fork() can reflink the lower+upper into the
6260
6271
  // bundle. Without this, `machinen snapshot <vm>` from
@@ -6265,19 +6276,20 @@ function registerInRegistry(args) {
6265
6276
  upperPath: args.mountDiskPaths.upperPath
6266
6277
  } : void 0,
6267
6278
  // #273: persist live-share mount config so an attach-owned
6268
- // snapshot/fork can write the same `meta.liveMounts` block
6269
- // and trigger /sbin/machinen-remount post-dump on
6270
- // leaveRunning paths. Host UDS / vsock port aren't carried —
6271
- // those are this process's private state and the attach side
6272
- // doesn't need them (the source's servers stay listening
6273
- // through the dump and the re-fork'd fuse-agent reconnects).
6274
- 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,
6275
6287
  startedAt: Date.now()
6276
6288
  });
6277
- debug11("registered pid=%d name=%s", args.childPid, args.vmName ?? "<unset>");
6289
+ debug10("registered pid=%d name=%s", args.childPid, args.vmName ?? "<unset>");
6278
6290
  return true;
6279
6291
  } catch (err) {
6280
- debug11(
6292
+ debug10(
6281
6293
  "registry write failed (best-effort) err=%s",
6282
6294
  err instanceof Error ? err.message : String(err)
6283
6295
  );
@@ -6286,7 +6298,7 @@ function registerInRegistry(args) {
6286
6298
  }
6287
6299
  function installVmExitCleanup(state) {
6288
6300
  state.child.once("exit", (code, signal) => {
6289
- debug11(
6301
+ debug10(
6290
6302
  "VMM exit pid=%d code=%s signal=%s lifetimeMs=%d",
6291
6303
  state.childPid,
6292
6304
  code,
@@ -6296,7 +6308,7 @@ function installVmExitCleanup(state) {
6296
6308
  for (const file of [state.perBootRootDisk, state.perBootSnapDisk, state.perBootMountUpper]) {
6297
6309
  if (file) {
6298
6310
  try {
6299
- unlinkSync7(file);
6311
+ unlinkSync8(file);
6300
6312
  } catch {
6301
6313
  }
6302
6314
  }
@@ -6304,7 +6316,7 @@ function installVmExitCleanup(state) {
6304
6316
  for (const dir of [state.bundleTempDir, state.vsockTempDir, state.statsTempDir]) {
6305
6317
  if (dir) {
6306
6318
  try {
6307
- rmSync9(dir, { recursive: true, force: true });
6319
+ rmSync8(dir, { recursive: true, force: true });
6308
6320
  } catch {
6309
6321
  }
6310
6322
  }
@@ -6312,15 +6324,37 @@ function installVmExitCleanup(state) {
6312
6324
  if (state.gvStop) {
6313
6325
  state.gvStop();
6314
6326
  }
6315
- for (const server of state.liveMountServers) {
6316
- void server.stop().catch(() => {
6317
- });
6318
- }
6319
6327
  if (state.registered) {
6320
6328
  removeEntry(state.childPid);
6321
6329
  }
6322
6330
  });
6323
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
+ }
6324
6358
  function installFlushPhases(child, phases, onLog) {
6325
6359
  let phasesFlushed = false;
6326
6360
  const flush = () => {
@@ -6329,7 +6363,7 @@ function installFlushPhases(child, phases, onLog) {
6329
6363
  }
6330
6364
  phasesFlushed = true;
6331
6365
  phases.end("first-guest-byte");
6332
- phases.flush(debug11, "boot");
6366
+ phases.flush(debug10, "boot");
6333
6367
  onLog?.(phases.toEvent("boot"));
6334
6368
  };
6335
6369
  child.stderr.once("data", flush);
@@ -6345,39 +6379,6 @@ function installDetachedBootCapture(child, sink) {
6345
6379
  }
6346
6380
  });
6347
6381
  }
6348
- async function spawnLiveMountServersForBoot(args) {
6349
- try {
6350
- for (const lm of args.liveMountsResolved) {
6351
- const lmHandle = await spawnDetachedMountServer({
6352
- udsPath: lm.udsPath,
6353
- rootAbs: lm.host,
6354
- mode: lm.mode,
6355
- vmmPid: args.childPid,
6356
- statsPath: lm.statsPath
6357
- });
6358
- args.liveMountServers.push(lmHandle);
6359
- }
6360
- } catch (err) {
6361
- try {
6362
- args.child.kill("SIGKILL");
6363
- } catch {
6364
- }
6365
- throw err;
6366
- }
6367
- if (!args.registered) {
6368
- return;
6369
- }
6370
- try {
6371
- patchEntry(args.childPid, {
6372
- liveMountServers: args.liveMountServers.map((h) => ({ pid: h.pid, exe: h.exe }))
6373
- });
6374
- } catch (err) {
6375
- debug11(
6376
- "registry patch (liveMountServers) failed (best-effort) err=%s",
6377
- err instanceof Error ? err.message : String(err)
6378
- );
6379
- }
6380
- }
6381
6382
  async function gateOnDetachedReadiness(args) {
6382
6383
  const readinessTimeoutMs = args.timeoutMs ?? 6e4;
6383
6384
  let onByte = null;
@@ -6462,14 +6463,17 @@ function makeKill(child) {
6462
6463
  }
6463
6464
 
6464
6465
  // src/vm/restore.ts
6465
- var debug12 = debugLib13("machinen:boot");
6466
- var debugRestore = debugLib13("machinen:restore");
6466
+ var debug11 = debugLib12("machinen:boot");
6467
+ var debugRestore = debugLib12("machinen:restore");
6467
6468
  async function restore(opts) {
6468
6469
  const phases = new PhaseTimer();
6469
6470
  const snapDir = resolve7(opts.snapDir);
6471
+ if (existsSync15(join14(snapDir, VMSTATE_FILE))) {
6472
+ return restoreVmstate(opts, snapDir);
6473
+ }
6470
6474
  const imgDir = join14(snapDir, "img");
6471
6475
  const metaPath = join14(snapDir, "meta.json");
6472
- if (!existsSync16(imgDir) || !statSync10(imgDir).isDirectory()) {
6476
+ if (!existsSync15(imgDir) || !statSync10(imgDir).isDirectory()) {
6473
6477
  throw new BootError("BOOT_SNAPSHOT_NOT_FOUND", `restore: ${imgDir} not found`);
6474
6478
  }
6475
6479
  const imgEntries = readdirSync7(imgDir);
@@ -6481,50 +6485,21 @@ async function restore(opts) {
6481
6485
  }
6482
6486
  phases.start("snapshot-meta-read");
6483
6487
  let meta = { snappedAt: 0 };
6484
- if (existsSync16(metaPath)) {
6488
+ if (existsSync15(metaPath)) {
6485
6489
  try {
6486
- meta = JSON.parse(readFileSync10(metaPath, "utf8"));
6490
+ meta = JSON.parse(readFileSync9(metaPath, "utf8"));
6487
6491
  } catch {
6488
6492
  }
6489
6493
  }
6490
6494
  phases.end("snapshot-meta-read");
6491
- let resolvedImage;
6492
- if (opts.image) {
6493
- resolvedImage = resolve7(opts.cwd ?? process.cwd(), opts.image);
6494
- if (!existsSync16(resolvedImage)) {
6495
- throw new BootError("BOOT_IMAGE_NOT_FOUND", `restore: image not found: ${resolvedImage}`);
6496
- }
6497
- } else if (meta.sourceImage && existsSync16(meta.sourceImage)) {
6498
- resolvedImage = meta.sourceImage;
6499
- debugRestore("using meta.sourceImage path=%s", resolvedImage);
6500
- } else if (meta.sourceImage) {
6501
- throw new BootError(
6502
- "BOOT_IMAGE_NOT_FOUND",
6503
- `restore: source image not found at ${meta.sourceImage}
6504
- The snapshot was taken with this rootfs tarball, and CRIU needs
6505
- it to reopen the process's file-backed memory mappings (e.g.
6506
- /usr/bin/node, libc, etc).
6507
- \u2022 copy the tarball to that path on this host, OR
6508
- \u2022 pass an explicit override via the runtime's restore({ image })
6509
- or the CLI's \`machinen restore --image <tarball>\`.`
6510
- );
6511
- } else {
6512
- throw new BootError(
6513
- "BOOT_IMAGE_NOT_FOUND",
6514
- `restore: no rootfs image available for this bundle.
6515
- The snapshot's meta.json doesn't record a source image (likely
6516
- predates the field). Pass the same tarball you booted the
6517
- source VM with via the runtime's restore({ image }) or the
6518
- CLI's \`machinen restore --image <tarball>\`.`
6519
- );
6520
- }
6495
+ const resolvedImage = resolveRestoreImage(opts, meta);
6521
6496
  const lazyPages = opts.lazy === true;
6522
6497
  let lazyPagesTotal;
6523
6498
  if (lazyPages) {
6524
6499
  phases.start("snapshot-mark-lazy");
6525
6500
  const marked = markPagemapsLazy(imgDir);
6526
6501
  lazyPagesTotal = marked.entriesFlagged + marked.entriesAlreadyLazy;
6527
- debug12(
6502
+ debug11(
6528
6503
  "lazy-pages mark: files=%d entriesFlagged=%d alreadyLazy=%d",
6529
6504
  marked.filesRewritten,
6530
6505
  marked.entriesFlagged,
@@ -6565,7 +6540,7 @@ async function restore(opts) {
6565
6540
  }
6566
6541
  } catch (err) {
6567
6542
  try {
6568
- unlinkSync8(scratchPath);
6543
+ unlinkSync9(scratchPath);
6569
6544
  } catch {
6570
6545
  }
6571
6546
  throw new BootError(
@@ -6580,7 +6555,7 @@ async function restore(opts) {
6580
6555
  if (meta.mountDisk) {
6581
6556
  const lowerAbs = join14(snapDir, meta.mountDisk.lower);
6582
6557
  const upperAbs = join14(snapDir, meta.mountDisk.upper);
6583
- if (!existsSync16(lowerAbs) || !existsSync16(upperAbs)) {
6558
+ if (!existsSync15(lowerAbs) || !existsSync15(upperAbs)) {
6584
6559
  throw new BootError(
6585
6560
  "BOOT_SNAPSHOT_NOT_FOUND",
6586
6561
  `restore: bundle's mount overlay is missing one of:
@@ -6609,31 +6584,18 @@ async function restore(opts) {
6609
6584
  });
6610
6585
  } finally {
6611
6586
  try {
6612
- unlinkSync8(scratchPath);
6587
+ unlinkSync9(scratchPath);
6613
6588
  } catch {
6614
6589
  }
6615
6590
  }
6616
6591
  phases.end("boot");
6617
- if (!opts.name && meta.sourceName) {
6618
- const candidates = [`${meta.sourceName}/${vm.pid}`, `${meta.sourceName}~${vm.pid}`];
6619
- for (const candidate of candidates) {
6620
- if (claimName(candidate, vm.pid)) {
6621
- const cur = findEntry({ pid: vm.pid });
6622
- if (cur) {
6623
- writeEntry({ ...cur, name: candidate });
6624
- }
6625
- vm.name = candidate;
6626
- break;
6627
- }
6628
- }
6629
- }
6592
+ autoNameRestoredFork(vm, opts, meta);
6630
6593
  if (lazyPagesTotal !== void 0) {
6631
6594
  const cur = findEntry({ pid: vm.pid });
6632
6595
  if (cur) {
6633
6596
  writeEntry({
6634
6597
  ...cur,
6635
- lazyPagesTotal,
6636
- lazyPagesMountRoot: imgDir
6598
+ lazyPagesTotal
6637
6599
  });
6638
6600
  }
6639
6601
  }
@@ -6644,9 +6606,98 @@ async function restore(opts) {
6644
6606
  });
6645
6607
  return vm;
6646
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
+ }
6647
6698
 
6648
6699
  // src/vm/fork.ts
6649
- var debugFork = debugLib14("machinen:fork");
6700
+ var debugFork = debugLib13("machinen:fork");
6650
6701
  async function performFork(ctx, opts) {
6651
6702
  await checkForkBackpressure({
6652
6703
  threshold: opts.freeMemoryThreshold ?? DEFAULT_FREE_MEMORY_THRESHOLD
@@ -6672,7 +6723,7 @@ async function performFork(ctx, opts) {
6672
6723
  } catch (err) {
6673
6724
  if (ephemeral) {
6674
6725
  try {
6675
- rmSync10(snapDir, { recursive: true, force: true });
6726
+ rmSync9(snapDir, { recursive: true, force: true });
6676
6727
  } catch {
6677
6728
  }
6678
6729
  }
@@ -6692,7 +6743,7 @@ async function performFork(ctx, opts) {
6692
6743
  } catch (err) {
6693
6744
  if (ephemeral) {
6694
6745
  try {
6695
- rmSync10(snapDir, { recursive: true, force: true });
6746
+ rmSync9(snapDir, { recursive: true, force: true });
6696
6747
  } catch {
6697
6748
  }
6698
6749
  }
@@ -6703,7 +6754,7 @@ async function performFork(ctx, opts) {
6703
6754
  void fork.wait().catch(() => {
6704
6755
  }).finally(() => {
6705
6756
  try {
6706
- rmSync10(snapDir, { recursive: true, force: true });
6757
+ rmSync9(snapDir, { recursive: true, force: true });
6707
6758
  debugFork("fork ephemeral bundle cleaned up snapDir=%s", snapDir);
6708
6759
  } catch (cleanupErr) {
6709
6760
  debugFork(
@@ -6725,7 +6776,7 @@ function measureFirstByte(vm) {
6725
6776
  }
6726
6777
 
6727
6778
  // src/vm/attach.ts
6728
- var debugAttach = debugLib15("machinen:attach");
6779
+ var debugAttach = debugLib14("machinen:attach");
6729
6780
  async function attach(opts) {
6730
6781
  debugAttach("attach lookup pid=%s name=%s", opts.pid ?? "<unset>", opts.name ?? "<unset>");
6731
6782
  const entry = findEntry(opts);
@@ -6746,7 +6797,7 @@ async function attach(opts) {
6746
6797
  const stderr = new PassThrough3();
6747
6798
  stdout.end();
6748
6799
  stderr.end();
6749
- const waitForExit2 = async () => {
6800
+ const waitForExit = async () => {
6750
6801
  while (isAlive(entry.pid)) {
6751
6802
  await new Promise((r) => setTimeout(r, 200));
6752
6803
  }
@@ -6759,7 +6810,7 @@ async function attach(opts) {
6759
6810
  stdout,
6760
6811
  stderr,
6761
6812
  async wait() {
6762
- return waitForExit2();
6813
+ return waitForExit();
6763
6814
  },
6764
6815
  async kill() {
6765
6816
  if (!isAlive(entry.pid)) {
@@ -6769,7 +6820,7 @@ async function attach(opts) {
6769
6820
  process.kill(entry.pid, "SIGKILL");
6770
6821
  } catch {
6771
6822
  }
6772
- await waitForExit2();
6823
+ await waitForExit();
6773
6824
  },
6774
6825
  async detach() {
6775
6826
  },
@@ -6812,7 +6863,8 @@ ${res.stderr}`
6812
6863
  };
6813
6864
  },
6814
6865
  async snapshot(snapshotOpts) {
6815
- if (!entry.diskPath) {
6866
+ const engine = resolveSnapshotEngine();
6867
+ if (engine === "criu" && !entry.diskPath || engine === "vmstate" && !entry.vmstatePath) {
6816
6868
  throw new SnapshotError(
6817
6869
  "SNAPSHOT_NO_DISK",
6818
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>'`."
@@ -6821,7 +6873,8 @@ ${res.stderr}`
6821
6873
  return performSnapshot(buildAttachSnapshotContext(), snapshotOpts);
6822
6874
  },
6823
6875
  async fork(forkOpts) {
6824
- if (!entry.diskPath) {
6876
+ const engine = resolveSnapshotEngine();
6877
+ if (engine === "criu" && !entry.diskPath || engine === "vmstate" && !entry.vmstatePath) {
6825
6878
  throw new SnapshotError(
6826
6879
  "SNAPSHOT_NO_DISK",
6827
6880
  "vm.fork: source VM has no scratch disk (booted with `snapshot: false`)."
@@ -6841,6 +6894,9 @@ ${res.stderr}`
6841
6894
  // bundle exactly like boot-owned snapshots do.
6842
6895
  mountDisk: entry.mountDisk,
6843
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,
6844
6900
  execRaw: (cmd, execOpts) => handle.execRaw(cmd, execOpts),
6845
6901
  wait: () => handle.wait(),
6846
6902
  kill: () => handle.kill(),
@@ -6862,8 +6918,8 @@ var _internal = {
6862
6918
  };
6863
6919
 
6864
6920
  // src/provision.ts
6865
- var debug13 = debugLib16("machinen:provision");
6866
- var vmmDebug2 = debugLib16("machinen:vmm");
6921
+ var debug12 = debugLib15("machinen:provision");
6922
+ var vmmDebug2 = debugLib15("machinen:vmm");
6867
6923
  var TAR_TO_DISK_CMD = [
6868
6924
  "tar",
6869
6925
  "-C /",
@@ -6923,7 +6979,7 @@ function resolveBaseDtb(explicit, cwd = process.cwd()) {
6923
6979
  function resolveBaseAsset(spec, explicit, cwd) {
6924
6980
  if (explicit) {
6925
6981
  const abs = resolve9(cwd, explicit);
6926
- if (!existsSync17(abs)) {
6982
+ if (!existsSync16(abs)) {
6927
6983
  throw new ProvisionError(spec.missingCode, `${spec.kind} not found: ${abs}`);
6928
6984
  }
6929
6985
  return abs;
@@ -6931,7 +6987,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6931
6987
  const assetsDir = process.env.MACHINEN_ASSETS_DIR;
6932
6988
  if (assetsDir) {
6933
6989
  const p = resolve9(assetsDir, spec.assetsDirName);
6934
- if (!existsSync17(p)) {
6990
+ if (!existsSync16(p)) {
6935
6991
  throw new ProvisionError(
6936
6992
  "PROVISION_ASSETS_DIR_INVALID",
6937
6993
  `MACHINEN_ASSETS_DIR=${assetsDir} does not contain ${spec.assetsDirName}`
@@ -6940,7 +6996,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6940
6996
  return p;
6941
6997
  }
6942
6998
  const cached = join16(cliCachedBaseDir(), spec.cliCacheName);
6943
- if (existsSync17(cached)) {
6999
+ if (existsSync16(cached)) {
6944
7000
  return cached;
6945
7001
  }
6946
7002
  throw new ProvisionError(
@@ -6953,7 +7009,7 @@ function resolveBaseAsset(spec, explicit, cwd) {
6953
7009
  }
6954
7010
  function cliCachedBaseDir() {
6955
7011
  const pkgPath = resolve9(import.meta.dirname, "..", "package.json");
6956
- const version = JSON.parse(readFileSync11(pkgPath, "utf8")).version;
7012
+ const version = JSON.parse(readFileSync10(pkgPath, "utf8")).version;
6957
7013
  return join16(homedir8(), ".machinen", `runtime-v${version}`, "bases", "debian-arm64");
6958
7014
  }
6959
7015
  async function provision(opts) {
@@ -6962,18 +7018,18 @@ async function provision(opts) {
6962
7018
  const kernelAbs = resolveBaseKernel(opts.kernel, cwd);
6963
7019
  const dtbAbs = resolveBaseDtb(opts.dtb, cwd);
6964
7020
  const outAbs = resolve9(cwd, opts.out);
6965
- mkdirSync12(dirname9(outAbs), { recursive: true });
7021
+ mkdirSync12(dirname7(outAbs), { recursive: true });
6966
7022
  const t0 = Date.now();
6967
7023
  const workDir = mkdtempSync8(join16(tmpdir8(), "machinen-provision-"));
6968
7024
  const diskPath = join16(workDir, "scratch.img");
6969
7025
  const rootDiskPath = join16(workDir, "rootfs.img");
6970
7026
  const udsPath = join16(workDir, "exec.sock");
6971
- 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);
6972
7028
  const phases = new PhaseTimer();
6973
7029
  try {
6974
7030
  const scratchBytes = opts.scratchDiskSizeBytes ?? 1024 * 1024 * 1024;
6975
7031
  allocateSparseFile4(diskPath, scratchBytes);
6976
- debug13("scratch disk allocated path=%s sizeBytes=%d", diskPath, scratchBytes);
7032
+ debug12("scratch disk allocated path=%s sizeBytes=%d", diskPath, scratchBytes);
6977
7033
  phases.start("rootdisk-prep");
6978
7034
  const cachedImg = ensureRootfsImage(baseAbs, {
6979
7035
  onPhase: (name, ms) => phases.mark(`rootdisk-prep.${name}`, ms)
@@ -6981,7 +7037,7 @@ async function provision(opts) {
6981
7037
  const reflinkT0 = Date.now();
6982
7038
  reflinkCopy(cachedImg, rootDiskPath);
6983
7039
  phases.mark("rootdisk-prep.reflink", Date.now() - reflinkT0);
6984
- debug13("rootdisk cloned src=%s dst=%s", cachedImg, rootDiskPath);
7040
+ debug12("rootdisk cloned src=%s dst=%s", cachedImg, rootDiskPath);
6985
7041
  markRootfsImageClean(cachedImg);
6986
7042
  phases.end("rootdisk-prep");
6987
7043
  phases.start("boot");
@@ -7027,14 +7083,14 @@ async function provision(opts) {
7027
7083
  const stderrTail = () => Buffer.concat(tailBuf).slice(-TAIL_MAX).toString("utf8");
7028
7084
  try {
7029
7085
  const installT0 = Date.now();
7030
- debug13("install hook entry");
7086
+ debug12("install hook entry");
7031
7087
  phases.start("install");
7032
7088
  try {
7033
7089
  await opts.install(vm);
7034
7090
  } catch (err) {
7035
7091
  const tail = stderrTail();
7036
7092
  const msg = err instanceof Error ? err.message : String(err);
7037
- debug13("install hook failed err=%s tailBytes=%d", msg, tail.length);
7093
+ debug12("install hook failed err=%s tailBytes=%d", msg, tail.length);
7038
7094
  throw new ProvisionError(
7039
7095
  "PROVISION_INSTALL_HOOK_FAILED",
7040
7096
  `install hook failed: ${msg}
@@ -7044,8 +7100,8 @@ ${tail}`,
7044
7100
  );
7045
7101
  }
7046
7102
  phases.end("install");
7047
- debug13("install hook done elapsed=%dms", Date.now() - installT0);
7048
- debug13("tar / -> /dev/vdb starting");
7103
+ debug12("install hook done elapsed=%dms", Date.now() - installT0);
7104
+ debug12("tar / -> /dev/vdb starting");
7049
7105
  const tarT0 = Date.now();
7050
7106
  phases.start("tar-to-disk");
7051
7107
  const tar = await VsockExec.run(udsPath, TAR_TO_DISK_CMD, {
@@ -7053,7 +7109,7 @@ ${tail}`,
7053
7109
  ...tapExecForLog(TAR_TO_DISK_CMD, opts.onLog)
7054
7110
  });
7055
7111
  phases.end("tar-to-disk");
7056
- 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);
7057
7113
  if (tar.exitCode !== 0) {
7058
7114
  throw new ProvisionError(
7059
7115
  "PROVISION_DISK_TOO_SMALL",
@@ -7062,7 +7118,7 @@ Bump scratchDiskSizeBytes. stderr:
7062
7118
  ${tar.stderr}`
7063
7119
  );
7064
7120
  }
7065
- debug13("requesting guest poweroff");
7121
+ debug12("requesting guest poweroff");
7066
7122
  phases.start("poweroff-wait");
7067
7123
  await VsockExec.run(udsPath, "/sbin/machinen-poweroff", {
7068
7124
  connectTimeoutMs: 2e3,
@@ -7072,7 +7128,7 @@ ${tar.stderr}`
7072
7128
  console.error("provision: waiting for guest exit\u2026");
7073
7129
  await vm.wait();
7074
7130
  phases.end("poweroff-wait");
7075
- debug13("guest exited");
7131
+ debug12("guest exited");
7076
7132
  } finally {
7077
7133
  clearTimeout(killTimer);
7078
7134
  if (vm.pid > 0) {
@@ -7080,7 +7136,7 @@ ${tar.stderr}`
7080
7136
  });
7081
7137
  }
7082
7138
  }
7083
- debug13("repack disk tar -> %s starting", outAbs);
7139
+ debug12("repack disk tar -> %s starting", outAbs);
7084
7140
  const repackT0 = Date.now();
7085
7141
  phases.start("repack-targz");
7086
7142
  repackDiskTarToGz(diskPath, outAbs, {
@@ -7089,7 +7145,7 @@ ${tar.stderr}`
7089
7145
  onPhase: (name, ms) => phases.mark(`repack-targz.${name}`, ms)
7090
7146
  });
7091
7147
  phases.end("repack-targz");
7092
- debug13("repack done elapsed=%dms", Date.now() - repackT0);
7148
+ debug12("repack done elapsed=%dms", Date.now() - repackT0);
7093
7149
  const sizeBytes = statSync11(outAbs).size;
7094
7150
  warmImageConfigCache(
7095
7151
  outAbs,
@@ -7099,8 +7155,8 @@ ${tar.stderr}`
7099
7155
  } : null
7100
7156
  );
7101
7157
  const elapsedMs = Date.now() - t0;
7102
- debug13("provision complete sizeBytes=%d totalElapsed=%dms", sizeBytes, elapsedMs);
7103
- phases.flush(debug13, "provision", elapsedMs);
7158
+ debug12("provision complete sizeBytes=%d totalElapsed=%dms", sizeBytes, elapsedMs);
7159
+ phases.flush(debug12, "provision", elapsedMs);
7104
7160
  opts.onLog?.(phases.toEvent("provision", elapsedMs));
7105
7161
  return {
7106
7162
  imagePath: outAbs,
@@ -7109,7 +7165,7 @@ ${tar.stderr}`
7109
7165
  };
7110
7166
  } finally {
7111
7167
  try {
7112
- rmSync11(workDir, { recursive: true, force: true });
7168
+ rmSync10(workDir, { recursive: true, force: true });
7113
7169
  } catch {
7114
7170
  }
7115
7171
  }
@@ -7160,7 +7216,7 @@ function repackDiskTarToGz(diskPath, outAbs, opts = {}) {
7160
7216
  });
7161
7217
  } finally {
7162
7218
  try {
7163
- rmSync11(extractDir, { recursive: true, force: true });
7219
+ rmSync10(extractDir, { recursive: true, force: true });
7164
7220
  } catch {
7165
7221
  }
7166
7222
  }