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