@machinen/runtime 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +127 -136
- package/dist/index.d.ts +90 -88
- package/dist/index.js +682 -626
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
- package/dist/chunk-6SP6T537.js +0 -163
- package/dist/chunk-6SP6T537.js.map +0 -1
- package/dist/mount-server-bin.d.ts +0 -2
- package/dist/mount-server-bin.js +0 -2065
- package/dist/mount-server-bin.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
1277
|
+
existsSync as existsSync16,
|
|
1162
1278
|
mkdirSync as mkdirSync12,
|
|
1163
1279
|
mkdtempSync as mkdtempSync8,
|
|
1164
1280
|
openSync as openSync7,
|
|
1165
|
-
readFileSync as
|
|
1166
|
-
rmSync as
|
|
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
|
|
1173
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2006
|
+
existsSync as existsSync15,
|
|
1900
2007
|
openSync as openSync6,
|
|
1901
2008
|
readdirSync as readdirSync7,
|
|
1902
|
-
readFileSync as
|
|
2009
|
+
readFileSync as readFileSync9,
|
|
1903
2010
|
statSync as statSync10,
|
|
1904
|
-
unlinkSync as
|
|
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
|
|
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
|
|
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
|
|
2301
|
+
existsSync as existsSync14,
|
|
2195
2302
|
mkdtempSync as mkdtempSync6,
|
|
2196
2303
|
openSync as openSync5,
|
|
2197
|
-
rmSync as
|
|
2198
|
-
unlinkSync as
|
|
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
|
|
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 = "
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
3367
|
-
import
|
|
3368
|
-
var
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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 &&
|
|
3418
|
-
|
|
3419
|
-
if (!
|
|
3420
|
-
|
|
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
|
-
|
|
3377
|
+
debug6("cache hit grew img=%s from=%d to=%d", imgPath, cur, opts.sizeBytes);
|
|
3437
3378
|
}
|
|
3438
3379
|
} catch (err) {
|
|
3439
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
3617
|
-
|
|
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/
|
|
3568
|
+
const pkg = `@machinen/native-${arch()}-${platform3()}`;
|
|
3628
3569
|
try {
|
|
3629
|
-
const
|
|
3630
|
-
|
|
3631
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3634
|
+
debug6("prebake done sha=%s img=%s", sha.slice(0, 12), imgPath);
|
|
3695
3635
|
return imgPath;
|
|
3696
3636
|
} catch (err) {
|
|
3697
|
-
|
|
3637
|
+
debug6("prebake error sibling=%s err=%s", sibling, err.message);
|
|
3698
3638
|
return void 0;
|
|
3699
3639
|
} finally {
|
|
3700
3640
|
try {
|
|
3701
|
-
|
|
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
|
-
|
|
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 (
|
|
3745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3712
|
+
debug6("prebake emitted cache=%s sizeBytes=%d", imgPath, sizeBytes);
|
|
3773
3713
|
} finally {
|
|
3774
3714
|
try {
|
|
3775
|
-
|
|
3715
|
+
rmSync4(stagingDir, { recursive: true, force: true });
|
|
3776
3716
|
} catch {
|
|
3777
3717
|
}
|
|
3778
3718
|
}
|
|
3779
3719
|
} catch (err) {
|
|
3780
|
-
|
|
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
|
|
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
|
|
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
|
|
3739
|
+
readFileSync as readFileSync7,
|
|
3800
3740
|
readlinkSync as readlinkSync2,
|
|
3801
|
-
rmSync as
|
|
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
|
|
3808
|
-
import { fileURLToPath
|
|
3809
|
-
import
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 ?
|
|
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:
|
|
4087
|
+
newc("init", 33261, { data: readFileSync7(initPath) })
|
|
4150
4088
|
];
|
|
4151
4089
|
appendFinalEntries(parts, {
|
|
4152
4090
|
initPath,
|
|
4153
|
-
config: opts.config ?
|
|
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 =
|
|
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 =
|
|
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/
|
|
4174
|
+
const pkgName = `@machinen/native-${osArch2()}-${osPlatform5()}`;
|
|
4244
4175
|
try {
|
|
4245
4176
|
const mod = require_3(pkgName);
|
|
4246
|
-
if (mod.initPath && mod.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 {
|
|
4398
|
-
import
|
|
4399
|
-
var
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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 &&
|
|
4455
|
-
|
|
4456
|
-
if (!
|
|
4457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
4564
|
-
|
|
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/
|
|
4500
|
+
const pkg = `@machinen/native-${arch2()}-${platform4()}`;
|
|
4575
4501
|
try {
|
|
4576
|
-
const
|
|
4577
|
-
|
|
4578
|
-
|
|
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 (
|
|
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
|
|
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
|
|
4711
|
-
var
|
|
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 (!
|
|
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/
|
|
4683
|
+
const pkgName = `@machinen/native-${key}`;
|
|
4759
4684
|
try {
|
|
4760
4685
|
const mod = require_5(pkgName);
|
|
4761
|
-
if (!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
|
-
|
|
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
|
-
|
|
4853
|
+
debug9("setGuestHostname: set pid=%d hostname=%s", vm.pid, hostname);
|
|
4929
4854
|
} catch (err) {
|
|
4930
|
-
|
|
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
|
|
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 &&
|
|
4896
|
+
if (cachePath && existsSync11(cachePath)) {
|
|
4972
4897
|
try {
|
|
4973
|
-
const raw =
|
|
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
|
|
5002
|
-
function resolveLiveMounts(mounts, cwd
|
|
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 (!
|
|
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
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
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((
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
5241
|
-
import {
|
|
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
|
|
5244
|
-
|
|
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
|
|
5264
|
+
function prepareBundleRootDir(outDir) {
|
|
5302
5265
|
const snapDir = resolve5(outDir);
|
|
5303
|
-
if (
|
|
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 =
|
|
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 }) => ({
|
|
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
|
|
5567
|
-
var vmmDebug =
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
5611
|
-
if ((
|
|
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
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
5875
|
+
lazyPagesPending: lazyTotal
|
|
5855
5876
|
};
|
|
5856
5877
|
},
|
|
5857
5878
|
async snapshot(snapshotOpts) {
|
|
5858
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
6270
|
-
//
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
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
|
-
|
|
6289
|
+
debug10("registered pid=%d name=%s", args.childPid, args.vmName ?? "<unset>");
|
|
6278
6290
|
return true;
|
|
6279
6291
|
} catch (err) {
|
|
6280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
6466
|
-
var debugRestore =
|
|
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 (!
|
|
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 (
|
|
6488
|
+
if (existsSync15(metaPath)) {
|
|
6485
6489
|
try {
|
|
6486
|
-
meta = JSON.parse(
|
|
6490
|
+
meta = JSON.parse(readFileSync9(metaPath, "utf8"));
|
|
6487
6491
|
} catch {
|
|
6488
6492
|
}
|
|
6489
6493
|
}
|
|
6490
6494
|
phases.end("snapshot-meta-read");
|
|
6491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
6587
|
+
unlinkSync9(scratchPath);
|
|
6613
6588
|
} catch {
|
|
6614
6589
|
}
|
|
6615
6590
|
}
|
|
6616
6591
|
phases.end("boot");
|
|
6617
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6866
|
-
var vmmDebug2 =
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7048
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7103
|
-
phases.flush(
|
|
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
|
-
|
|
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
|
-
|
|
7219
|
+
rmSync10(extractDir, { recursive: true, force: true });
|
|
7164
7220
|
} catch {
|
|
7165
7221
|
}
|
|
7166
7222
|
}
|