@machinen/cli 0.3.3 → 0.4.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/README.md +6 -12
- package/dist/cli.js +1854 -1217
- package/dist/cli.js.map +1 -1
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
symlinkSync,
|
|
14
14
|
unlinkSync
|
|
15
15
|
} from "fs";
|
|
16
|
-
import { homedir as homedir2, tmpdir } from "os";
|
|
16
|
+
import { arch as osArch, homedir as homedir2, tmpdir } from "os";
|
|
17
17
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
18
18
|
import { PassThrough, Transform } from "stream";
|
|
19
19
|
import { pipeline } from "stream/promises";
|
|
@@ -33,7 +33,7 @@ import debugLib2 from "debug";
|
|
|
33
33
|
// package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "@machinen/cli",
|
|
36
|
-
version: "0.
|
|
36
|
+
version: "0.4.0",
|
|
37
37
|
license: "FSL-1.1-MIT",
|
|
38
38
|
repository: {
|
|
39
39
|
type: "git",
|
|
@@ -64,7 +64,8 @@ var package_default = {
|
|
|
64
64
|
},
|
|
65
65
|
optionalDependencies: {
|
|
66
66
|
"@machinen/native-arm64-darwin": "workspace:*",
|
|
67
|
-
"@machinen/native-arm64-linux": "workspace:*"
|
|
67
|
+
"@machinen/native-arm64-linux": "workspace:*",
|
|
68
|
+
"@machinen/native-x64-linux": "workspace:*"
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
71
|
|
|
@@ -95,7 +96,7 @@ var COMMANDS = [
|
|
|
95
96
|
name: "--mount-live",
|
|
96
97
|
type: "string",
|
|
97
98
|
repeatable: true,
|
|
98
|
-
description: "Live-share host dir. Spec: <host>:<guest>[:rw|ro]
|
|
99
|
+
description: "Live-share host dir over virtio-fs. Spec: <host>:<guest>[:rw|ro]."
|
|
99
100
|
},
|
|
100
101
|
{
|
|
101
102
|
name: "--env",
|
|
@@ -122,6 +123,11 @@ var COMMANDS = [
|
|
|
122
123
|
description: "Detach the VMM from the CLI on first-guest-byte readiness."
|
|
123
124
|
},
|
|
124
125
|
{ name: "--memory", type: "integer", description: "Guest RAM ceiling in MiB (debug knob)." },
|
|
126
|
+
{
|
|
127
|
+
name: "--nested",
|
|
128
|
+
type: "boolean",
|
|
129
|
+
description: "Expose arm64 EL2 / /dev/kvm to the guest when the host supports it."
|
|
130
|
+
},
|
|
125
131
|
{
|
|
126
132
|
name: "--json",
|
|
127
133
|
type: "boolean",
|
|
@@ -144,7 +150,7 @@ var COMMANDS = [
|
|
|
144
150
|
{
|
|
145
151
|
name: "--lazy",
|
|
146
152
|
type: "boolean",
|
|
147
|
-
description: "Opt into lazy-pages restore (#266) \u2014
|
|
153
|
+
description: "Opt into CRIU lazy-pages restore (#266) \u2014 virtio-fs mount the bundle and fault pages on demand. Default is eager."
|
|
148
154
|
},
|
|
149
155
|
{
|
|
150
156
|
name: "-p",
|
|
@@ -237,7 +243,7 @@ var COMMANDS = [
|
|
|
237
243
|
{
|
|
238
244
|
name: "--lazy",
|
|
239
245
|
type: "boolean",
|
|
240
|
-
description: "Opt into lazy-pages restore (#266);
|
|
246
|
+
description: "Opt into CRIU lazy-pages restore (#266); the CLI currently ignores this when --detach is set."
|
|
241
247
|
},
|
|
242
248
|
{
|
|
243
249
|
name: "-p",
|
|
@@ -486,120 +492,175 @@ import { ParseError as ParseError2 } from "@machinen/runtime";
|
|
|
486
492
|
|
|
487
493
|
// src/parse-run-args.ts
|
|
488
494
|
import { ParseError } from "@machinen/runtime";
|
|
495
|
+
var RUN_VALUE_FLAGS = /* @__PURE__ */ new Map([
|
|
496
|
+
["--mount", "mount"],
|
|
497
|
+
["--mount-live", "liveMount"],
|
|
498
|
+
["--env", "env"],
|
|
499
|
+
["-p", "portForward"],
|
|
500
|
+
["--publish", "portForward"],
|
|
501
|
+
["--snapshot", "snapshot"],
|
|
502
|
+
["--cwd", "guestCwd"],
|
|
503
|
+
["--name", "name"],
|
|
504
|
+
["--memory", "memory"]
|
|
505
|
+
]);
|
|
506
|
+
var RUN_BARE_FLAGS = /* @__PURE__ */ new Map([
|
|
507
|
+
["--nested", "nested"],
|
|
508
|
+
["--detached", "detach"],
|
|
509
|
+
["--detach", "detach"],
|
|
510
|
+
["--json", "json"]
|
|
511
|
+
]);
|
|
512
|
+
var RUN_FLAG_HANDLERS = {
|
|
513
|
+
mount: handleRunMount,
|
|
514
|
+
liveMount: handleRunLiveMount,
|
|
515
|
+
env: handleRunEnv,
|
|
516
|
+
portForward: handleRunPortForward,
|
|
517
|
+
snapshot: handleRunSnapshot,
|
|
518
|
+
nested: handleRunNested,
|
|
519
|
+
nestedValue: handleRunNestedValue,
|
|
520
|
+
guestCwd: handleRunGuestCwd,
|
|
521
|
+
name: handleRunName,
|
|
522
|
+
detach: handleRunDetach,
|
|
523
|
+
json: handleRunJson,
|
|
524
|
+
memory: handleRunMemory
|
|
525
|
+
};
|
|
489
526
|
function parseRunArgs(argv) {
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const double_dash_args = idx === -1 ? [] : argv.slice(idx + 1);
|
|
493
|
-
const positional = [];
|
|
494
|
-
let mount;
|
|
495
|
-
const liveMounts = [];
|
|
496
|
-
const env = {};
|
|
497
|
-
const portForward = [];
|
|
498
|
-
const seenHostPorts = /* @__PURE__ */ new Set();
|
|
499
|
-
let snapshot;
|
|
500
|
-
let name;
|
|
501
|
-
let guestCwd;
|
|
502
|
-
let detached = false;
|
|
503
|
-
let memory;
|
|
504
|
-
let json = false;
|
|
527
|
+
const { pre, double_dash_args } = splitCommandArgs(argv);
|
|
528
|
+
const state = newRunParseState();
|
|
505
529
|
for (let i = 0; i < pre.length; i++) {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
"--mount may be given at most once per invocation"
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
const r = consumeMount(a, pre, i);
|
|
515
|
-
mount = r.value;
|
|
516
|
-
i = r.next;
|
|
517
|
-
} else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
|
|
518
|
-
const r = consumeLiveMount(a, pre, i);
|
|
519
|
-
liveMounts.push(r.value);
|
|
520
|
-
i = r.next;
|
|
521
|
-
} else if (a === "--env" || a.startsWith("--env=")) {
|
|
522
|
-
const r = consumeEnv(a, pre, i);
|
|
523
|
-
env[r.key] = r.value;
|
|
524
|
-
i = r.next;
|
|
525
|
-
} else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
|
|
526
|
-
i = consumePortForward(a, pre, i, seenHostPorts, portForward);
|
|
527
|
-
} else if (a === "--snapshot" || a.startsWith("--snapshot=")) {
|
|
528
|
-
if (snapshot) {
|
|
529
|
-
throw new ParseError(
|
|
530
|
-
"PARSE_FLAG_DUPLICATE",
|
|
531
|
-
"--snapshot may be given at most once per invocation"
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
const { spec, next } = takeValue(a, pre, i, "a path value");
|
|
535
|
-
snapshot = spec;
|
|
536
|
-
i = next;
|
|
537
|
-
} else if (a === "--cwd" || a.startsWith("--cwd=")) {
|
|
538
|
-
if (guestCwd !== void 0) {
|
|
539
|
-
throw new ParseError(
|
|
540
|
-
"PARSE_FLAG_DUPLICATE",
|
|
541
|
-
"--cwd may be given at most once per invocation"
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
const r = consumeGuestCwd(a, pre, i);
|
|
545
|
-
guestCwd = r.value;
|
|
546
|
-
i = r.next;
|
|
547
|
-
} else if (a === "--name" || a.startsWith("--name=")) {
|
|
548
|
-
if (name) {
|
|
549
|
-
throw new ParseError(
|
|
550
|
-
"PARSE_FLAG_DUPLICATE",
|
|
551
|
-
"--name may be given at most once per invocation"
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
const { spec, next } = takeValue(a, pre, i, "a value");
|
|
555
|
-
name = spec;
|
|
556
|
-
i = next;
|
|
557
|
-
} else if (a === "--detached" || a === "--detach") {
|
|
558
|
-
if (detached) {
|
|
559
|
-
throw new ParseError(
|
|
560
|
-
"PARSE_FLAG_DUPLICATE",
|
|
561
|
-
"--detached may be given at most once per invocation"
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
detached = true;
|
|
565
|
-
if (a === "--detached" && !process.env.MACHINEN_QUIET_DEPRECATIONS && !process.env.VITEST) {
|
|
566
|
-
process.stderr.write(
|
|
567
|
-
"machinen: --detached is deprecated; use --detach (same behaviour).\n"
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
} else if (a === "--json") {
|
|
571
|
-
json = true;
|
|
572
|
-
} else if (a === "--memory" || a.startsWith("--memory=")) {
|
|
573
|
-
if (memory !== void 0) {
|
|
574
|
-
throw new ParseError(
|
|
575
|
-
"PARSE_FLAG_DUPLICATE",
|
|
576
|
-
"--memory may be given at most once per invocation"
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
const r = consumeMemory(a, pre, i);
|
|
580
|
-
memory = r.value;
|
|
581
|
-
i = r.next;
|
|
582
|
-
} else if (a.startsWith("-")) {
|
|
583
|
-
throw new ParseError("PARSE_FLAG_UNKNOWN", `unknown flag: ${a}`);
|
|
584
|
-
} else {
|
|
585
|
-
positional.push(a);
|
|
530
|
+
const arg = pre[i];
|
|
531
|
+
const flag = runFlagFor(arg);
|
|
532
|
+
if (flag) {
|
|
533
|
+
i = RUN_FLAG_HANDLERS[flag](state, arg, pre, i);
|
|
534
|
+
continue;
|
|
586
535
|
}
|
|
536
|
+
if (arg.startsWith("-")) {
|
|
537
|
+
throw new ParseError("PARSE_FLAG_UNKNOWN", `unknown flag: ${arg}`);
|
|
538
|
+
}
|
|
539
|
+
state.positional.push(arg);
|
|
540
|
+
}
|
|
541
|
+
return finishRunArgs(state, double_dash_args);
|
|
542
|
+
}
|
|
543
|
+
function splitCommandArgs(argv) {
|
|
544
|
+
const idx = argv.indexOf("--");
|
|
545
|
+
if (idx === -1) {
|
|
546
|
+
return { pre: argv, double_dash_args: [] };
|
|
547
|
+
}
|
|
548
|
+
return { pre: argv.slice(0, idx), double_dash_args: argv.slice(idx + 1) };
|
|
549
|
+
}
|
|
550
|
+
function newRunParseState() {
|
|
551
|
+
return {
|
|
552
|
+
positional: [],
|
|
553
|
+
liveMounts: [],
|
|
554
|
+
env: {},
|
|
555
|
+
portForward: [],
|
|
556
|
+
seenHostPorts: /* @__PURE__ */ new Set(),
|
|
557
|
+
nested: false,
|
|
558
|
+
detached: false,
|
|
559
|
+
json: false
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function runFlagFor(arg) {
|
|
563
|
+
const eq = arg.indexOf("=");
|
|
564
|
+
if (eq !== -1) {
|
|
565
|
+
const head = arg.slice(0, eq);
|
|
566
|
+
if (head === "--nested") {
|
|
567
|
+
return "nestedValue";
|
|
568
|
+
}
|
|
569
|
+
return RUN_VALUE_FLAGS.get(head);
|
|
587
570
|
}
|
|
571
|
+
return RUN_BARE_FLAGS.get(arg) ?? RUN_VALUE_FLAGS.get(arg);
|
|
572
|
+
}
|
|
573
|
+
function finishRunArgs(state, double_dash_args) {
|
|
588
574
|
return {
|
|
589
|
-
positional,
|
|
575
|
+
positional: state.positional,
|
|
590
576
|
double_dash_args,
|
|
591
|
-
mount,
|
|
592
|
-
liveMounts: liveMounts.length > 0 ? liveMounts : void 0,
|
|
593
|
-
env: Object.keys(env).length > 0 ? env : void 0,
|
|
594
|
-
portForward: portForward.length > 0 ? portForward : void 0,
|
|
595
|
-
snapshot,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
577
|
+
mount: state.mount,
|
|
578
|
+
liveMounts: state.liveMounts.length > 0 ? state.liveMounts : void 0,
|
|
579
|
+
env: Object.keys(state.env).length > 0 ? state.env : void 0,
|
|
580
|
+
portForward: state.portForward.length > 0 ? state.portForward : void 0,
|
|
581
|
+
snapshot: state.snapshot,
|
|
582
|
+
nested: state.nested || void 0,
|
|
583
|
+
name: state.name,
|
|
584
|
+
guestCwd: state.guestCwd,
|
|
585
|
+
detached: state.detached || void 0,
|
|
586
|
+
memory: state.memory,
|
|
587
|
+
json: state.json || void 0
|
|
601
588
|
};
|
|
602
589
|
}
|
|
590
|
+
function handleRunMount(state, flag, args, index) {
|
|
591
|
+
assertRunFlagUnused(state.mount !== void 0, "--mount");
|
|
592
|
+
const result = consumeMount(flag, args, index);
|
|
593
|
+
state.mount = result.value;
|
|
594
|
+
return result.next;
|
|
595
|
+
}
|
|
596
|
+
function handleRunLiveMount(state, flag, args, index) {
|
|
597
|
+
const result = consumeLiveMount(flag, args, index);
|
|
598
|
+
state.liveMounts.push(result.value);
|
|
599
|
+
return result.next;
|
|
600
|
+
}
|
|
601
|
+
function handleRunEnv(state, flag, args, index) {
|
|
602
|
+
const result = consumeEnv(flag, args, index);
|
|
603
|
+
state.env[result.key] = result.value;
|
|
604
|
+
return result.next;
|
|
605
|
+
}
|
|
606
|
+
function handleRunPortForward(state, flag, args, index) {
|
|
607
|
+
return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
|
|
608
|
+
}
|
|
609
|
+
function handleRunSnapshot(state, flag, args, index) {
|
|
610
|
+
assertRunFlagUnused(state.snapshot !== void 0, "--snapshot");
|
|
611
|
+
const { spec, next } = takeValue(flag, args, index, "a path value");
|
|
612
|
+
state.snapshot = spec;
|
|
613
|
+
return next;
|
|
614
|
+
}
|
|
615
|
+
function handleRunNested(state, _flag, _args, index) {
|
|
616
|
+
assertRunFlagUnused(state.nested, "--nested");
|
|
617
|
+
state.nested = true;
|
|
618
|
+
return index;
|
|
619
|
+
}
|
|
620
|
+
function handleRunNestedValue() {
|
|
621
|
+
throw new ParseError("PARSE_FLAG_MALFORMED", "--nested does not take a value");
|
|
622
|
+
}
|
|
623
|
+
function handleRunGuestCwd(state, flag, args, index) {
|
|
624
|
+
assertRunFlagUnused(state.guestCwd !== void 0, "--cwd");
|
|
625
|
+
const result = consumeGuestCwd(flag, args, index);
|
|
626
|
+
state.guestCwd = result.value;
|
|
627
|
+
return result.next;
|
|
628
|
+
}
|
|
629
|
+
function handleRunName(state, flag, args, index) {
|
|
630
|
+
assertRunFlagUnused(state.name !== void 0, "--name");
|
|
631
|
+
const { spec, next } = takeValue(flag, args, index, "a value");
|
|
632
|
+
state.name = spec;
|
|
633
|
+
return next;
|
|
634
|
+
}
|
|
635
|
+
function handleRunDetach(state, flag, _args, index) {
|
|
636
|
+
assertRunFlagUnused(state.detached, "--detached");
|
|
637
|
+
state.detached = true;
|
|
638
|
+
warnDeprecatedDetached(flag);
|
|
639
|
+
return index;
|
|
640
|
+
}
|
|
641
|
+
function handleRunJson(state, _flag, _args, index) {
|
|
642
|
+
state.json = true;
|
|
643
|
+
return index;
|
|
644
|
+
}
|
|
645
|
+
function handleRunMemory(state, flag, args, index) {
|
|
646
|
+
assertRunFlagUnused(state.memory !== void 0, "--memory");
|
|
647
|
+
const result = consumeMemory(flag, args, index);
|
|
648
|
+
state.memory = result.value;
|
|
649
|
+
return result.next;
|
|
650
|
+
}
|
|
651
|
+
function assertRunFlagUnused(used, flag) {
|
|
652
|
+
if (used) {
|
|
653
|
+
throw new ParseError(
|
|
654
|
+
"PARSE_FLAG_DUPLICATE",
|
|
655
|
+
`${flag} may be given at most once per invocation`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function warnDeprecatedDetached(flag) {
|
|
660
|
+
if (flag === "--detached" && !process.env.MACHINEN_QUIET_DEPRECATIONS && !process.env.VITEST) {
|
|
661
|
+
process.stderr.write("machinen: --detached is deprecated; use --detach (same behaviour).\n");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
603
664
|
function parsePort(raw, label) {
|
|
604
665
|
if (!/^[0-9]+$/.test(raw)) {
|
|
605
666
|
throw new ParseError("PARSE_PORT_INVALID", `-p: ${label} must be numeric (got '${raw}')`);
|
|
@@ -715,167 +776,255 @@ function consumeGuestCwd(flag, args, i) {
|
|
|
715
776
|
}
|
|
716
777
|
|
|
717
778
|
// src/parse-fork-args.ts
|
|
779
|
+
var FORK_VALUE_FLAGS = /* @__PURE__ */ new Map([
|
|
780
|
+
["--new-name", "newName"],
|
|
781
|
+
["--out-dir", "outDir"],
|
|
782
|
+
["-p", "portForward"],
|
|
783
|
+
["--publish", "portForward"],
|
|
784
|
+
["--mount", "mount"],
|
|
785
|
+
["--mount-live", "liveMount"],
|
|
786
|
+
["--env", "env"],
|
|
787
|
+
["--cwd", "guestCwd"],
|
|
788
|
+
["--memory", "memory"]
|
|
789
|
+
]);
|
|
790
|
+
var FORK_BARE_FLAGS = /* @__PURE__ */ new Map([
|
|
791
|
+
["--tcp-keep", "tcpKeep"],
|
|
792
|
+
["--detach", "detach"],
|
|
793
|
+
["--lazy", "lazy"]
|
|
794
|
+
]);
|
|
795
|
+
var FORK_FLAG_HANDLERS = {
|
|
796
|
+
newName: handleForkNewName,
|
|
797
|
+
outDir: handleForkOutDir,
|
|
798
|
+
tcpKeep: handleForkTcpKeep,
|
|
799
|
+
detach: handleForkDetach,
|
|
800
|
+
lazy: handleForkLazy,
|
|
801
|
+
portForward: handleForkPortForward,
|
|
802
|
+
mount: handleForkMount,
|
|
803
|
+
liveMount: handleForkLiveMount,
|
|
804
|
+
env: handleForkEnv,
|
|
805
|
+
guestCwd: handleForkGuestCwd,
|
|
806
|
+
memory: handleForkMemory
|
|
807
|
+
};
|
|
718
808
|
function parseForkArgs(argv) {
|
|
719
|
-
|
|
720
|
-
let outDir;
|
|
721
|
-
let tcpKeep = false;
|
|
722
|
-
let detach = false;
|
|
723
|
-
let lazy = false;
|
|
724
|
-
const portForward = [];
|
|
725
|
-
const seenHostPorts = /* @__PURE__ */ new Set();
|
|
726
|
-
let mount;
|
|
727
|
-
const liveMounts = [];
|
|
728
|
-
const env = {};
|
|
729
|
-
let guestCwd;
|
|
730
|
-
let memory;
|
|
731
|
-
const rest = [];
|
|
809
|
+
const state = newForkParseState();
|
|
732
810
|
for (let i = 0; i < argv.length; i++) {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
"--new-name may be given at most once per invocation"
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
const { spec, next } = takeValue(a, argv, i, "a value");
|
|
742
|
-
newName = spec;
|
|
743
|
-
i = next;
|
|
744
|
-
} else if (a === "--out-dir" || a.startsWith("--out-dir=")) {
|
|
745
|
-
if (outDir !== void 0) {
|
|
746
|
-
throw new ParseError2(
|
|
747
|
-
"PARSE_FLAG_DUPLICATE",
|
|
748
|
-
"--out-dir may be given at most once per invocation"
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
const { spec, next } = takeValue(a, argv, i, "a directory path");
|
|
752
|
-
outDir = spec;
|
|
753
|
-
i = next;
|
|
754
|
-
} else if (a === "--tcp-keep") {
|
|
755
|
-
tcpKeep = true;
|
|
756
|
-
} else if (a === "--detach") {
|
|
757
|
-
detach = true;
|
|
758
|
-
} else if (a === "--lazy") {
|
|
759
|
-
lazy = true;
|
|
760
|
-
} else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
|
|
761
|
-
i = consumePortForward(a, argv, i, seenHostPorts, portForward);
|
|
762
|
-
} else if (a === "--mount" || a.startsWith("--mount=")) {
|
|
763
|
-
if (mount) {
|
|
764
|
-
throw new ParseError2(
|
|
765
|
-
"PARSE_FLAG_DUPLICATE",
|
|
766
|
-
"--mount may be given at most once per invocation"
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
const r = consumeMount(a, argv, i);
|
|
770
|
-
mount = r.value;
|
|
771
|
-
i = r.next;
|
|
772
|
-
} else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
|
|
773
|
-
const r = consumeLiveMount(a, argv, i);
|
|
774
|
-
liveMounts.push(r.value);
|
|
775
|
-
i = r.next;
|
|
776
|
-
} else if (a === "--env" || a.startsWith("--env=")) {
|
|
777
|
-
const r = consumeEnv(a, argv, i);
|
|
778
|
-
env[r.key] = r.value;
|
|
779
|
-
i = r.next;
|
|
780
|
-
} else if (a === "--cwd" || a.startsWith("--cwd=")) {
|
|
781
|
-
if (guestCwd !== void 0) {
|
|
782
|
-
throw new ParseError2(
|
|
783
|
-
"PARSE_FLAG_DUPLICATE",
|
|
784
|
-
"--cwd may be given at most once per invocation"
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
const r = consumeGuestCwd(a, argv, i);
|
|
788
|
-
guestCwd = r.value;
|
|
789
|
-
i = r.next;
|
|
790
|
-
} else if (a === "--memory" || a.startsWith("--memory=")) {
|
|
791
|
-
if (memory !== void 0) {
|
|
792
|
-
throw new ParseError2(
|
|
793
|
-
"PARSE_FLAG_DUPLICATE",
|
|
794
|
-
"--memory may be given at most once per invocation"
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
const r = consumeMemory(a, argv, i);
|
|
798
|
-
memory = r.value;
|
|
799
|
-
i = r.next;
|
|
800
|
-
} else {
|
|
801
|
-
rest.push(a);
|
|
811
|
+
const arg = argv[i];
|
|
812
|
+
const flag = forkFlagFor(arg);
|
|
813
|
+
if (flag) {
|
|
814
|
+
i = FORK_FLAG_HANDLERS[flag](state, arg, argv, i);
|
|
815
|
+
continue;
|
|
802
816
|
}
|
|
817
|
+
state.rest.push(arg);
|
|
818
|
+
}
|
|
819
|
+
return finishForkArgs(state);
|
|
820
|
+
}
|
|
821
|
+
function newForkParseState() {
|
|
822
|
+
return {
|
|
823
|
+
tcpKeep: false,
|
|
824
|
+
detach: false,
|
|
825
|
+
lazy: false,
|
|
826
|
+
portForward: [],
|
|
827
|
+
seenHostPorts: /* @__PURE__ */ new Set(),
|
|
828
|
+
liveMounts: [],
|
|
829
|
+
env: {},
|
|
830
|
+
rest: []
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function forkFlagFor(arg) {
|
|
834
|
+
const eq = arg.indexOf("=");
|
|
835
|
+
if (eq !== -1) {
|
|
836
|
+
return FORK_VALUE_FLAGS.get(arg.slice(0, eq));
|
|
803
837
|
}
|
|
838
|
+
return FORK_BARE_FLAGS.get(arg) ?? FORK_VALUE_FLAGS.get(arg);
|
|
839
|
+
}
|
|
840
|
+
function finishForkArgs(state) {
|
|
804
841
|
return {
|
|
805
|
-
newName,
|
|
806
|
-
outDir,
|
|
807
|
-
tcpKeep,
|
|
808
|
-
detach,
|
|
809
|
-
lazy: lazy && !detach,
|
|
810
|
-
portForward,
|
|
811
|
-
mount,
|
|
812
|
-
liveMounts: liveMounts.length > 0 ? liveMounts : void 0,
|
|
813
|
-
env: Object.keys(env).length > 0 ? env : void 0,
|
|
814
|
-
guestCwd,
|
|
815
|
-
memory,
|
|
816
|
-
rest
|
|
842
|
+
newName: state.newName,
|
|
843
|
+
outDir: state.outDir,
|
|
844
|
+
tcpKeep: state.tcpKeep,
|
|
845
|
+
detach: state.detach,
|
|
846
|
+
lazy: state.lazy && !state.detach,
|
|
847
|
+
portForward: state.portForward,
|
|
848
|
+
mount: state.mount,
|
|
849
|
+
liveMounts: state.liveMounts.length > 0 ? state.liveMounts : void 0,
|
|
850
|
+
env: Object.keys(state.env).length > 0 ? state.env : void 0,
|
|
851
|
+
guestCwd: state.guestCwd,
|
|
852
|
+
memory: state.memory,
|
|
853
|
+
rest: state.rest
|
|
817
854
|
};
|
|
818
855
|
}
|
|
856
|
+
function handleForkNewName(state, flag, args, index) {
|
|
857
|
+
assertForkFlagUnused(state.newName !== void 0, "--new-name");
|
|
858
|
+
const { spec, next } = takeValue(flag, args, index, "a value");
|
|
859
|
+
state.newName = spec;
|
|
860
|
+
return next;
|
|
861
|
+
}
|
|
862
|
+
function handleForkOutDir(state, flag, args, index) {
|
|
863
|
+
assertForkFlagUnused(state.outDir !== void 0, "--out-dir");
|
|
864
|
+
const { spec, next } = takeValue(flag, args, index, "a directory path");
|
|
865
|
+
state.outDir = spec;
|
|
866
|
+
return next;
|
|
867
|
+
}
|
|
868
|
+
function handleForkTcpKeep(state, _flag, _args, index) {
|
|
869
|
+
state.tcpKeep = true;
|
|
870
|
+
return index;
|
|
871
|
+
}
|
|
872
|
+
function handleForkDetach(state, _flag, _args, index) {
|
|
873
|
+
state.detach = true;
|
|
874
|
+
return index;
|
|
875
|
+
}
|
|
876
|
+
function handleForkLazy(state, _flag, _args, index) {
|
|
877
|
+
state.lazy = true;
|
|
878
|
+
return index;
|
|
879
|
+
}
|
|
880
|
+
function handleForkPortForward(state, flag, args, index) {
|
|
881
|
+
return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
|
|
882
|
+
}
|
|
883
|
+
function handleForkMount(state, flag, args, index) {
|
|
884
|
+
assertForkFlagUnused(state.mount !== void 0, "--mount");
|
|
885
|
+
const result = consumeMount(flag, args, index);
|
|
886
|
+
state.mount = result.value;
|
|
887
|
+
return result.next;
|
|
888
|
+
}
|
|
889
|
+
function handleForkLiveMount(state, flag, args, index) {
|
|
890
|
+
const result = consumeLiveMount(flag, args, index);
|
|
891
|
+
state.liveMounts.push(result.value);
|
|
892
|
+
return result.next;
|
|
893
|
+
}
|
|
894
|
+
function handleForkEnv(state, flag, args, index) {
|
|
895
|
+
const result = consumeEnv(flag, args, index);
|
|
896
|
+
state.env[result.key] = result.value;
|
|
897
|
+
return result.next;
|
|
898
|
+
}
|
|
899
|
+
function handleForkGuestCwd(state, flag, args, index) {
|
|
900
|
+
assertForkFlagUnused(state.guestCwd !== void 0, "--cwd");
|
|
901
|
+
const result = consumeGuestCwd(flag, args, index);
|
|
902
|
+
state.guestCwd = result.value;
|
|
903
|
+
return result.next;
|
|
904
|
+
}
|
|
905
|
+
function handleForkMemory(state, flag, args, index) {
|
|
906
|
+
assertForkFlagUnused(state.memory !== void 0, "--memory");
|
|
907
|
+
const result = consumeMemory(flag, args, index);
|
|
908
|
+
state.memory = result.value;
|
|
909
|
+
return result.next;
|
|
910
|
+
}
|
|
911
|
+
function assertForkFlagUnused(used, flag) {
|
|
912
|
+
if (used) {
|
|
913
|
+
throw new ParseError2(
|
|
914
|
+
"PARSE_FLAG_DUPLICATE",
|
|
915
|
+
`${flag} may be given at most once per invocation`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
819
919
|
|
|
820
920
|
// src/parse-restore-args.ts
|
|
821
921
|
import { ParseError as ParseError3 } from "@machinen/runtime";
|
|
922
|
+
var RESTORE_VALUE_FLAGS = /* @__PURE__ */ new Map([
|
|
923
|
+
["--name", "name"],
|
|
924
|
+
["--image", "image"],
|
|
925
|
+
["--mount-live", "liveMount"],
|
|
926
|
+
["-p", "portForward"],
|
|
927
|
+
["--publish", "portForward"]
|
|
928
|
+
]);
|
|
929
|
+
var RESTORE_BARE_FLAGS = /* @__PURE__ */ new Map([["--lazy", "lazy"]]);
|
|
930
|
+
var RESTORE_FLAG_HANDLERS = {
|
|
931
|
+
lazy: handleRestoreLazy,
|
|
932
|
+
name: handleRestoreName,
|
|
933
|
+
image: handleRestoreImage,
|
|
934
|
+
liveMount: handleRestoreLiveMount,
|
|
935
|
+
portForward: handleRestorePortForward
|
|
936
|
+
};
|
|
822
937
|
function parseRestoreArgs(argv) {
|
|
823
|
-
const
|
|
824
|
-
let name;
|
|
825
|
-
let image;
|
|
826
|
-
let lazy = false;
|
|
827
|
-
const portForward = [];
|
|
828
|
-
const liveMounts = [];
|
|
829
|
-
const seenLiveGuests = /* @__PURE__ */ new Set();
|
|
830
|
-
const seenHostPorts = /* @__PURE__ */ new Set();
|
|
938
|
+
const state = newRestoreParseState();
|
|
831
939
|
for (let i = 0; i < argv.length; i++) {
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (!v) {
|
|
838
|
-
throw new ParseError3("PARSE_FLAG_MISSING_VALUE", "--name requires a value");
|
|
839
|
-
}
|
|
840
|
-
if (name !== void 0) {
|
|
841
|
-
throw new ParseError3(
|
|
842
|
-
"PARSE_FLAG_DUPLICATE",
|
|
843
|
-
"--name may be given at most once per invocation"
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
name = v;
|
|
847
|
-
} else if (a === "--image" || a.startsWith("--image=")) {
|
|
848
|
-
const v = a === "--image" ? argv[++i] : a.slice("--image=".length);
|
|
849
|
-
if (!v) {
|
|
850
|
-
throw new ParseError3("PARSE_FLAG_MISSING_VALUE", "--image requires a value");
|
|
851
|
-
}
|
|
852
|
-
if (image !== void 0) {
|
|
853
|
-
throw new ParseError3(
|
|
854
|
-
"PARSE_FLAG_DUPLICATE",
|
|
855
|
-
"--image may be given at most once per invocation"
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
image = v;
|
|
859
|
-
} else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
|
|
860
|
-
const { value, next } = consumeLiveMount(a, argv, i);
|
|
861
|
-
i = next;
|
|
862
|
-
if (seenLiveGuests.has(value.guest)) {
|
|
863
|
-
throw new ParseError3(
|
|
864
|
-
"PARSE_FLAG_DUPLICATE",
|
|
865
|
-
`--mount-live override for guest=${value.guest} given more than once`
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
seenLiveGuests.add(value.guest);
|
|
869
|
-
liveMounts.push(value);
|
|
870
|
-
} else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
|
|
871
|
-
i = consumePortForward(a, argv, i, seenHostPorts, portForward);
|
|
872
|
-
} else if (a.startsWith("-")) {
|
|
873
|
-
throw new ParseError3("PARSE_FLAG_UNKNOWN", `unknown flag: ${a}`);
|
|
874
|
-
} else {
|
|
875
|
-
positional.push(a);
|
|
940
|
+
const arg = argv[i];
|
|
941
|
+
const flag = restoreFlagFor(arg);
|
|
942
|
+
if (flag) {
|
|
943
|
+
i = RESTORE_FLAG_HANDLERS[flag](state, arg, argv, i);
|
|
944
|
+
continue;
|
|
876
945
|
}
|
|
946
|
+
if (arg.startsWith("-")) {
|
|
947
|
+
throw new ParseError3("PARSE_FLAG_UNKNOWN", `unknown flag: ${arg}`);
|
|
948
|
+
}
|
|
949
|
+
state.positional.push(arg);
|
|
950
|
+
}
|
|
951
|
+
return finishRestoreArgs(state);
|
|
952
|
+
}
|
|
953
|
+
function newRestoreParseState() {
|
|
954
|
+
return {
|
|
955
|
+
positional: [],
|
|
956
|
+
portForward: [],
|
|
957
|
+
lazy: false,
|
|
958
|
+
liveMounts: [],
|
|
959
|
+
seenLiveGuests: /* @__PURE__ */ new Set(),
|
|
960
|
+
seenHostPorts: /* @__PURE__ */ new Set()
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function restoreFlagFor(arg) {
|
|
964
|
+
const eq = arg.indexOf("=");
|
|
965
|
+
if (eq !== -1) {
|
|
966
|
+
return RESTORE_VALUE_FLAGS.get(arg.slice(0, eq));
|
|
967
|
+
}
|
|
968
|
+
return RESTORE_BARE_FLAGS.get(arg) ?? RESTORE_VALUE_FLAGS.get(arg);
|
|
969
|
+
}
|
|
970
|
+
function finishRestoreArgs(state) {
|
|
971
|
+
return {
|
|
972
|
+
positional: state.positional,
|
|
973
|
+
name: state.name,
|
|
974
|
+
image: state.image,
|
|
975
|
+
portForward: state.portForward,
|
|
976
|
+
lazy: state.lazy,
|
|
977
|
+
liveMounts: state.liveMounts
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function handleRestoreLazy(state, _flag, _args, index) {
|
|
981
|
+
state.lazy = true;
|
|
982
|
+
return index;
|
|
983
|
+
}
|
|
984
|
+
function handleRestoreName(state, flag, args, index) {
|
|
985
|
+
const { spec, next } = takeRestoreValue(flag, args, index, "a value", "--name");
|
|
986
|
+
assertRestoreFlagUnused(state.name !== void 0, "--name");
|
|
987
|
+
state.name = spec;
|
|
988
|
+
return next;
|
|
989
|
+
}
|
|
990
|
+
function handleRestoreImage(state, flag, args, index) {
|
|
991
|
+
const { spec, next } = takeRestoreValue(flag, args, index, "a value", "--image");
|
|
992
|
+
assertRestoreFlagUnused(state.image !== void 0, "--image");
|
|
993
|
+
state.image = spec;
|
|
994
|
+
return next;
|
|
995
|
+
}
|
|
996
|
+
function handleRestoreLiveMount(state, flag, args, index) {
|
|
997
|
+
const { value, next } = consumeLiveMount(flag, args, index);
|
|
998
|
+
assertRestoreLiveGuestUnused(state, value.guest);
|
|
999
|
+
state.seenLiveGuests.add(value.guest);
|
|
1000
|
+
state.liveMounts.push(value);
|
|
1001
|
+
return next;
|
|
1002
|
+
}
|
|
1003
|
+
function handleRestorePortForward(state, flag, args, index) {
|
|
1004
|
+
return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
|
|
1005
|
+
}
|
|
1006
|
+
function takeRestoreValue(flag, args, index, label, displayFlag) {
|
|
1007
|
+
const result = takeValue(flag, args, index, label);
|
|
1008
|
+
if (!result.spec) {
|
|
1009
|
+
throw new ParseError3("PARSE_FLAG_MISSING_VALUE", `${displayFlag} requires ${label}`);
|
|
1010
|
+
}
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
function assertRestoreFlagUnused(used, flag) {
|
|
1014
|
+
if (used) {
|
|
1015
|
+
throw new ParseError3(
|
|
1016
|
+
"PARSE_FLAG_DUPLICATE",
|
|
1017
|
+
`${flag} may be given at most once per invocation`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function assertRestoreLiveGuestUnused(state, guest) {
|
|
1022
|
+
if (state.seenLiveGuests.has(guest)) {
|
|
1023
|
+
throw new ParseError3(
|
|
1024
|
+
"PARSE_FLAG_DUPLICATE",
|
|
1025
|
+
`--mount-live override for guest=${guest} given more than once`
|
|
1026
|
+
);
|
|
877
1027
|
}
|
|
878
|
-
return { positional, name, image, portForward, lazy, liveMounts };
|
|
879
1028
|
}
|
|
880
1029
|
|
|
881
1030
|
// src/parse-target.ts
|
|
@@ -1095,37 +1244,69 @@ function printDiagnostics(summary, opts = {}) {
|
|
|
1095
1244
|
if (!isQuiet()) {
|
|
1096
1245
|
return;
|
|
1097
1246
|
}
|
|
1098
|
-
const
|
|
1247
|
+
const buffer = diagnosticsBuffer(opts.buffer);
|
|
1099
1248
|
const tails = opts.tails ?? {};
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1249
|
+
printDiagnosticsEnvelope(buffer, tails);
|
|
1250
|
+
printDiagnosticsHint(opts.hint);
|
|
1251
|
+
}
|
|
1252
|
+
function diagnosticsBuffer(buffer) {
|
|
1253
|
+
if (typeof buffer === "string") {
|
|
1254
|
+
return buffer;
|
|
1255
|
+
}
|
|
1256
|
+
if (buffer === void 0) {
|
|
1257
|
+
return "";
|
|
1258
|
+
}
|
|
1259
|
+
return buffer.toString();
|
|
1260
|
+
}
|
|
1261
|
+
function printDiagnosticsEnvelope(buffer, tails) {
|
|
1262
|
+
const hasBuffer = hasDiagnosticsContent(buffer);
|
|
1263
|
+
if (!hasBuffer && !hasDiagnosticsTails(tails)) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
process.stderr.write("\n--- diagnostics ---\n");
|
|
1267
|
+
printDiagnosticsBuffer(buffer, hasBuffer);
|
|
1268
|
+
printDiagnosticsTails(tails, hasBuffer);
|
|
1269
|
+
process.stderr.write("--------------------\n\n");
|
|
1270
|
+
}
|
|
1271
|
+
function hasDiagnosticsContent(content) {
|
|
1272
|
+
if (!content) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
return content.trim().length > 0;
|
|
1276
|
+
}
|
|
1277
|
+
function hasDiagnosticsTails(tails) {
|
|
1278
|
+
return Object.values(tails).some(hasDiagnosticsContent);
|
|
1279
|
+
}
|
|
1280
|
+
function printDiagnosticsBuffer(buffer, hasBuffer) {
|
|
1281
|
+
if (!hasBuffer) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
process.stderr.write(buffer);
|
|
1285
|
+
writeTrailingNewline(buffer);
|
|
1286
|
+
}
|
|
1287
|
+
function printDiagnosticsTails(tails, afterBuffer) {
|
|
1288
|
+
for (const [label, content] of Object.entries(tails)) {
|
|
1289
|
+
if (!hasDiagnosticsContent(content)) {
|
|
1290
|
+
continue;
|
|
1109
1291
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
if (hasBuf) {
|
|
1115
|
-
process.stderr.write("\n");
|
|
1116
|
-
}
|
|
1117
|
-
process.stderr.write(`[${label}]
|
|
1118
|
-
`);
|
|
1119
|
-
process.stderr.write(content);
|
|
1120
|
-
if (!content.endsWith("\n")) {
|
|
1121
|
-
process.stderr.write("\n");
|
|
1122
|
-
}
|
|
1292
|
+
if (afterBuffer) {
|
|
1293
|
+
process.stderr.write("\n");
|
|
1123
1294
|
}
|
|
1124
|
-
process.stderr.write(
|
|
1295
|
+
process.stderr.write(`[${label}]
|
|
1296
|
+
`);
|
|
1297
|
+
process.stderr.write(content);
|
|
1298
|
+
writeTrailingNewline(content);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function writeTrailingNewline(content) {
|
|
1302
|
+
if (!content.endsWith("\n")) {
|
|
1303
|
+
process.stderr.write("\n");
|
|
1125
1304
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1305
|
+
}
|
|
1306
|
+
function printDiagnosticsHint(hint) {
|
|
1307
|
+
const text = hint ?? ESCAPE_HINT;
|
|
1308
|
+
if (text.length > 0) {
|
|
1309
|
+
process.stderr.write(text);
|
|
1129
1310
|
}
|
|
1130
1311
|
}
|
|
1131
1312
|
|
|
@@ -1155,51 +1336,93 @@ var CACHE_ROOT = join2(homedir2(), ".machinen");
|
|
|
1155
1336
|
function cacheDirFor(tag) {
|
|
1156
1337
|
return join2(CACHE_ROOT, tag);
|
|
1157
1338
|
}
|
|
1158
|
-
function
|
|
1339
|
+
function guestCpu() {
|
|
1340
|
+
const override = process.env.MACHINEN_GUEST_ARCH;
|
|
1341
|
+
if (override === "arm64" || override === "amd64") {
|
|
1342
|
+
return override;
|
|
1343
|
+
}
|
|
1344
|
+
return osArch() === "x64" ? "amd64" : "arm64";
|
|
1345
|
+
}
|
|
1346
|
+
function baseAssetSpec() {
|
|
1347
|
+
return guestCpu() === "amd64" ? {
|
|
1348
|
+
cpu: "amd64",
|
|
1349
|
+
kernelAsset: "bzImage-x86_64",
|
|
1350
|
+
rootfsAsset: "rootfs-debian-amd64.tar.gz"
|
|
1351
|
+
} : {
|
|
1352
|
+
cpu: "arm64",
|
|
1353
|
+
kernelAsset: "Image-arm64",
|
|
1354
|
+
dtbAsset: "virt-arm64.dtb",
|
|
1355
|
+
rootfsAsset: "rootfs-debian-arm64.tar.gz"
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function baseDirFor(tag, distro = "debian", cpu = guestCpu()) {
|
|
1159
1359
|
return join2(cacheDirFor(tag), "bases", `${distro}-${cpu}`);
|
|
1160
1360
|
}
|
|
1161
1361
|
function baseAssetsComplete(tag) {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1362
|
+
const spec = baseAssetSpec();
|
|
1363
|
+
const base = baseDirFor(tag, "debian", spec.cpu);
|
|
1364
|
+
return existsSync2(join2(base, "Image")) && (!spec.dtbAsset || existsSync2(join2(base, "virt.dtb"))) && existsSync2(join2(base, "rootfs.tar.gz"));
|
|
1164
1365
|
}
|
|
1165
|
-
var ASSETS_DIR_FILES = ["Image-arm64", "virt-arm64.dtb", "rootfs-debian-arm64.tar.gz"];
|
|
1166
1366
|
function validateAssetsDir(dir) {
|
|
1167
1367
|
const abs = resolve(dir);
|
|
1168
1368
|
if (!existsSync2(abs)) {
|
|
1169
1369
|
die(`MACHINEN_ASSETS_DIR=${dir} does not exist`);
|
|
1170
1370
|
}
|
|
1171
|
-
const
|
|
1371
|
+
const spec = baseAssetSpec();
|
|
1372
|
+
const required = [spec.kernelAsset, spec.rootfsAsset, ...spec.dtbAsset ? [spec.dtbAsset] : []];
|
|
1373
|
+
const missing = required.filter((f) => !existsSync2(join2(abs, f)));
|
|
1172
1374
|
if (missing.length > 0) {
|
|
1173
1375
|
die(
|
|
1174
|
-
`MACHINEN_ASSETS_DIR=${dir} is missing: ${missing.join(", ")}
|
|
1376
|
+
`MACHINEN_ASSETS_DIR=${dir} is missing for ${spec.cpu}: ${missing.join(", ")}
|
|
1175
1377
|
Produce them with ./scripts/build-base-assets.sh (outputs to ./release-assets/).`
|
|
1176
1378
|
);
|
|
1177
1379
|
}
|
|
1178
1380
|
}
|
|
1179
1381
|
async function ensureBaseAssets(tag) {
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
const tarball = join2(base, "rootfs.tar.gz");
|
|
1184
|
-
if (existsSync2(kernel) && existsSync2(dtb) && existsSync2(tarball)) {
|
|
1382
|
+
const spec = baseAssetSpec();
|
|
1383
|
+
const base = baseDirFor(tag, "debian", spec.cpu);
|
|
1384
|
+
if (cachedBaseAssetsReady(base, spec)) {
|
|
1185
1385
|
return base;
|
|
1186
1386
|
}
|
|
1187
1387
|
mkdirSync2(base, { recursive: true });
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1388
|
+
await downloadBaseAssets(tag, base, spec);
|
|
1389
|
+
replaceCurrentBaseSymlink(tag);
|
|
1390
|
+
return base;
|
|
1391
|
+
}
|
|
1392
|
+
function cachedBaseAssetsReady(base, spec) {
|
|
1393
|
+
if (!existsSync2(join2(base, "Image"))) {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
if (spec.dtbAsset && !existsSync2(join2(base, "virt.dtb"))) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
return existsSync2(join2(base, "rootfs.tar.gz"));
|
|
1400
|
+
}
|
|
1401
|
+
async function downloadBaseAssets(tag, base, spec) {
|
|
1402
|
+
await Promise.all(
|
|
1403
|
+
baseAssetDownloads(base, spec).map((a) => downloadWithChecksum(a.name, a.dest, tag))
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
function baseAssetDownloads(base, spec) {
|
|
1407
|
+
const assets = [{ name: spec.kernelAsset, dest: join2(base, "Image") }];
|
|
1408
|
+
if (spec.dtbAsset) {
|
|
1409
|
+
assets.push({ name: spec.dtbAsset, dest: join2(base, "virt.dtb") });
|
|
1410
|
+
}
|
|
1411
|
+
assets.push({ name: spec.rootfsAsset, dest: join2(base, "rootfs.tar.gz") });
|
|
1412
|
+
return assets;
|
|
1413
|
+
}
|
|
1414
|
+
function replaceCurrentBaseSymlink(tag) {
|
|
1194
1415
|
const current = join2(CACHE_ROOT, "current");
|
|
1195
1416
|
try {
|
|
1196
|
-
|
|
1197
|
-
unlinkSync(current);
|
|
1198
|
-
}
|
|
1417
|
+
unlinkCurrentSymlink(current);
|
|
1199
1418
|
} catch {
|
|
1200
1419
|
}
|
|
1201
1420
|
symlinkSync(tag, current, "dir");
|
|
1202
|
-
|
|
1421
|
+
}
|
|
1422
|
+
function unlinkCurrentSymlink(current) {
|
|
1423
|
+
if (existsSync2(current) || isSymlink(current)) {
|
|
1424
|
+
unlinkSync(current);
|
|
1425
|
+
}
|
|
1203
1426
|
}
|
|
1204
1427
|
function isSymlink(p) {
|
|
1205
1428
|
try {
|
|
@@ -1343,36 +1566,7 @@ function emitJson(value) {
|
|
|
1343
1566
|
function emitJsonError(code, message) {
|
|
1344
1567
|
process.stderr.write(JSON.stringify({ schema_version: 1, error: { code, message } }) + "\n");
|
|
1345
1568
|
}
|
|
1346
|
-
async function
|
|
1347
|
-
let parsed;
|
|
1348
|
-
try {
|
|
1349
|
-
parsed = parseRunArgs(args);
|
|
1350
|
-
} catch (err) {
|
|
1351
|
-
handleError(err);
|
|
1352
|
-
}
|
|
1353
|
-
const {
|
|
1354
|
-
positional,
|
|
1355
|
-
double_dash_args,
|
|
1356
|
-
mount,
|
|
1357
|
-
liveMounts,
|
|
1358
|
-
env,
|
|
1359
|
-
portForward,
|
|
1360
|
-
snapshot,
|
|
1361
|
-
name,
|
|
1362
|
-
guestCwd,
|
|
1363
|
-
detached,
|
|
1364
|
-
memory,
|
|
1365
|
-
json
|
|
1366
|
-
} = parsed;
|
|
1367
|
-
if (json && !detached) {
|
|
1368
|
-
die("boot --json is only meaningful with --detach (attached boots take over stdio).");
|
|
1369
|
-
}
|
|
1370
|
-
if (positional.length > 1) {
|
|
1371
|
-
die(
|
|
1372
|
-
"usage: machinen boot [<image>] [--snapshot <path>] [--name <name>] [--cwd <abs-path>] [--mount ...] [--mount-live ...] [--env KEY=VALUE]... [--detached] [--memory <mib>] [-- <cmd> [args...]]"
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
const imageOverride = positional[0];
|
|
1569
|
+
async function resolveCliBaseAssets() {
|
|
1376
1570
|
const assetsOverride = process.env.MACHINEN_ASSETS_DIR;
|
|
1377
1571
|
if (assetsOverride) {
|
|
1378
1572
|
validateAssetsDir(assetsOverride);
|
|
@@ -1381,311 +1575,462 @@ async function cmdBoot(args) {
|
|
|
1381
1575
|
`);
|
|
1382
1576
|
await ensureBaseAssets(RELEASE_TAG);
|
|
1383
1577
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
);
|
|
1391
|
-
const imagePath = imageOverride ? resolve(imageOverride) : defaultImagePath;
|
|
1392
|
-
debug(
|
|
1393
|
-
"boot baseDir=%s kernel=%s dtb=%s image=%s snapshot=%s name=%s",
|
|
1578
|
+
return cliBaseAssetPaths(assetsOverride);
|
|
1579
|
+
}
|
|
1580
|
+
function cliBaseAssetPaths(assetsOverride) {
|
|
1581
|
+
const spec = baseAssetSpec();
|
|
1582
|
+
const baseDir = cliBaseDir(assetsOverride, spec.cpu);
|
|
1583
|
+
return {
|
|
1394
1584
|
baseDir,
|
|
1395
|
-
kernelPath,
|
|
1396
|
-
dtbPath,
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const showHeadlines = isQuiet() && !(detached && json);
|
|
1404
|
-
const bootT0 = Date.now();
|
|
1405
|
-
const buffer = new RingBuffer();
|
|
1406
|
-
let filter = null;
|
|
1407
|
-
let filterOut = null;
|
|
1408
|
-
if (showHeadlines) {
|
|
1409
|
-
printHeadline(`booting ${headlineName}\u2026`);
|
|
1410
|
-
if (!detached) {
|
|
1411
|
-
filterOut = new PassThrough();
|
|
1412
|
-
filter = new NoiseFilter({
|
|
1413
|
-
buffer,
|
|
1414
|
-
out: filterOut,
|
|
1415
|
-
onReady: () => {
|
|
1416
|
-
printHeadline("guest ready");
|
|
1417
|
-
printHeadline(`ready in ${formatElapsed(Date.now() - bootT0)}`);
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1585
|
+
kernelPath: cliKernelPath(baseDir, assetsOverride, spec),
|
|
1586
|
+
dtbPath: cliDtbPath(baseDir, assetsOverride, spec),
|
|
1587
|
+
defaultImagePath: cliRootfsPath(baseDir, assetsOverride, spec)
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function cliBaseDir(assetsOverride, cpu) {
|
|
1591
|
+
if (assetsOverride) {
|
|
1592
|
+
return resolve(assetsOverride);
|
|
1421
1593
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
} : void 0;
|
|
1431
|
-
let vm;
|
|
1432
|
-
try {
|
|
1433
|
-
vm = await boot({
|
|
1434
|
-
// Always pass the base rootfs so /sbin/machinen-restore and
|
|
1435
|
-
// friends are in the initramfs even on a bare `machinen restore
|
|
1436
|
-
// <snap>` (no --image, no -- cmd).
|
|
1437
|
-
image: imagePath,
|
|
1438
|
-
cmd,
|
|
1439
|
-
env,
|
|
1440
|
-
kernel: kernelPath,
|
|
1441
|
-
dtb: dtbPath,
|
|
1442
|
-
mount,
|
|
1443
|
-
liveMounts,
|
|
1444
|
-
portForward,
|
|
1445
|
-
snapshot,
|
|
1446
|
-
name,
|
|
1447
|
-
guestCwd,
|
|
1448
|
-
detached,
|
|
1449
|
-
memory,
|
|
1450
|
-
onLog,
|
|
1451
|
-
// Interactive CLI: the session lives as long as the guest does.
|
|
1452
|
-
// Don't impose the default 60s cap. Detached boots fall back to
|
|
1453
|
-
// the runtime's own readiness timeout (60s) so the CLI can't
|
|
1454
|
-
// hang forever waiting for first-guest-byte.
|
|
1455
|
-
timeoutMs: detached ? void 0 : null
|
|
1456
|
-
});
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
filter?.flush();
|
|
1459
|
-
if (showHeadlines) {
|
|
1460
|
-
failQuiet(`boot ${headlineName} failed: ${describeError(err)}`, {
|
|
1461
|
-
buffer
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
handleError(err);
|
|
1594
|
+
return baseDirFor(RELEASE_TAG, "debian", cpu);
|
|
1595
|
+
}
|
|
1596
|
+
function cliKernelPath(baseDir, assetsOverride, spec) {
|
|
1597
|
+
return join2(baseDir, assetsOverride ? spec.kernelAsset : "Image");
|
|
1598
|
+
}
|
|
1599
|
+
function cliDtbPath(baseDir, assetsOverride, spec) {
|
|
1600
|
+
if (!spec.dtbAsset) {
|
|
1601
|
+
return void 0;
|
|
1465
1602
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
Stop: kill ${vm.pid} (machinen stop ships in PR2)
|
|
1479
|
-
`
|
|
1480
|
-
);
|
|
1481
|
-
}
|
|
1482
|
-
return 0;
|
|
1603
|
+
return join2(baseDir, assetsOverride ? spec.dtbAsset : "virt.dtb");
|
|
1604
|
+
}
|
|
1605
|
+
function cliRootfsPath(baseDir, assetsOverride, spec) {
|
|
1606
|
+
return join2(baseDir, assetsOverride ? spec.rootfsAsset : "rootfs.tar.gz");
|
|
1607
|
+
}
|
|
1608
|
+
function resolveOptionalImageOverride(imageOverride) {
|
|
1609
|
+
if (!imageOverride) {
|
|
1610
|
+
return void 0;
|
|
1611
|
+
}
|
|
1612
|
+
const imagePath = resolve(imageOverride);
|
|
1613
|
+
if (!existsSync2(imagePath)) {
|
|
1614
|
+
die(`--image: file not found: ${imagePath}`);
|
|
1483
1615
|
}
|
|
1616
|
+
return imagePath;
|
|
1617
|
+
}
|
|
1618
|
+
async function runAttachedVmSession(vm, opts) {
|
|
1484
1619
|
vm.stdout.pipe(process.stdout);
|
|
1485
|
-
if (!filter) {
|
|
1620
|
+
if (!opts.filter) {
|
|
1486
1621
|
vm.stderr.pipe(process.stderr);
|
|
1487
1622
|
}
|
|
1488
1623
|
const restoreStdin = rawModeStdinIfTTY();
|
|
1489
1624
|
const cancelHintRepeat = printCtrlDHint();
|
|
1490
|
-
|
|
1625
|
+
const signalState = installVmSignalHandlers(vm);
|
|
1626
|
+
pipeStdinToVm(vm.stdin, () => {
|
|
1627
|
+
process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
|
|
1628
|
+
signalState.forwardedSignal = "SIGTERM";
|
|
1629
|
+
void vm.kill();
|
|
1630
|
+
});
|
|
1631
|
+
opts.filterOut?.pipe(process.stderr);
|
|
1632
|
+
try {
|
|
1633
|
+
return await waitForAttachedVm(vm, opts, signalState);
|
|
1634
|
+
} finally {
|
|
1635
|
+
signalState.remove();
|
|
1636
|
+
cancelHintRepeat();
|
|
1637
|
+
restoreStdin();
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function installVmSignalHandlers(vm) {
|
|
1641
|
+
const state = { forwardedSignal: null, remove: () => {
|
|
1642
|
+
} };
|
|
1491
1643
|
const onSigint = () => {
|
|
1492
|
-
forwardedSignal = "SIGINT";
|
|
1644
|
+
state.forwardedSignal = "SIGINT";
|
|
1493
1645
|
void vm.kill();
|
|
1494
1646
|
};
|
|
1495
1647
|
const onSigterm = () => {
|
|
1496
|
-
forwardedSignal = "SIGTERM";
|
|
1648
|
+
state.forwardedSignal = "SIGTERM";
|
|
1497
1649
|
void vm.kill();
|
|
1498
1650
|
};
|
|
1499
1651
|
process.on("SIGINT", onSigint);
|
|
1500
1652
|
process.on("SIGTERM", onSigterm);
|
|
1501
|
-
|
|
1502
|
-
process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
|
|
1503
|
-
forwardedSignal = "SIGTERM";
|
|
1504
|
-
void vm.kill();
|
|
1505
|
-
});
|
|
1506
|
-
filterOut?.pipe(process.stderr);
|
|
1507
|
-
try {
|
|
1508
|
-
const { code } = await vm.wait();
|
|
1509
|
-
filter?.flush();
|
|
1510
|
-
if (forwardedSignal === "SIGINT") {
|
|
1511
|
-
return 130;
|
|
1512
|
-
}
|
|
1513
|
-
if (forwardedSignal === "SIGTERM") {
|
|
1514
|
-
return 143;
|
|
1515
|
-
}
|
|
1516
|
-
if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
|
|
1517
|
-
printDiagnostics(`boot ${headlineName} exited ${code} before reaching ready`, { buffer });
|
|
1518
|
-
}
|
|
1519
|
-
return code ?? 0;
|
|
1520
|
-
} finally {
|
|
1653
|
+
state.remove = () => {
|
|
1521
1654
|
process.off("SIGINT", onSigint);
|
|
1522
1655
|
process.off("SIGTERM", onSigterm);
|
|
1523
|
-
|
|
1524
|
-
|
|
1656
|
+
};
|
|
1657
|
+
return state;
|
|
1658
|
+
}
|
|
1659
|
+
async function waitForAttachedVm(vm, opts, signalState) {
|
|
1660
|
+
const { code } = await vm.wait();
|
|
1661
|
+
opts.filter?.flush();
|
|
1662
|
+
const signalExitCode = forwardedSignalExitCode(signalState.forwardedSignal);
|
|
1663
|
+
if (signalExitCode !== void 0) {
|
|
1664
|
+
return signalExitCode;
|
|
1665
|
+
}
|
|
1666
|
+
if (shouldPrintPreReadyDiagnostics(opts.filter, code, signalState.forwardedSignal)) {
|
|
1667
|
+
printDiagnostics(opts.preReadyExitSummary(code), { buffer: opts.buffer });
|
|
1525
1668
|
}
|
|
1669
|
+
return code ?? 0;
|
|
1526
1670
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const wasComplete = baseAssetsComplete(tag);
|
|
1531
|
-
const t0 = Date.now();
|
|
1532
|
-
if (!json) {
|
|
1533
|
-
process.stderr.write(`installing base assets for ${tag}\u2026
|
|
1534
|
-
`);
|
|
1535
|
-
if (!isQuiet()) {
|
|
1536
|
-
process.stderr.write(` into ${cacheDirFor(tag)}
|
|
1537
|
-
`);
|
|
1538
|
-
}
|
|
1671
|
+
function forwardedSignalExitCode(signal) {
|
|
1672
|
+
if (signal === "SIGINT") {
|
|
1673
|
+
return 130;
|
|
1539
1674
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
base = await ensureBaseAssets(tag);
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
if (isQuiet() && !json) {
|
|
1545
|
-
failQuiet(`install ${tag} failed: ${describeError(err)}`);
|
|
1546
|
-
}
|
|
1547
|
-
throw err;
|
|
1675
|
+
if (signal === "SIGTERM") {
|
|
1676
|
+
return 143;
|
|
1548
1677
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
fetched: !wasComplete
|
|
1555
|
-
});
|
|
1556
|
-
} else if (wasComplete) {
|
|
1557
|
-
process.stderr.write(`ready: ${base} (cached)
|
|
1558
|
-
`);
|
|
1559
|
-
} else {
|
|
1560
|
-
process.stderr.write(`ready in ${formatElapsed(Date.now() - t0)}: ${base}
|
|
1561
|
-
`);
|
|
1678
|
+
return void 0;
|
|
1679
|
+
}
|
|
1680
|
+
function shouldPrintPreReadyDiagnostics(filter, code, forwardedSignal) {
|
|
1681
|
+
if (!filter) {
|
|
1682
|
+
return false;
|
|
1562
1683
|
}
|
|
1563
|
-
|
|
1684
|
+
if (filter.ready || forwardedSignal) {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
return isNonZeroExit(code);
|
|
1564
1688
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1689
|
+
function isNonZeroExit(code) {
|
|
1690
|
+
if (code === null) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
return code !== 0;
|
|
1694
|
+
}
|
|
1695
|
+
async function cmdBoot(args) {
|
|
1696
|
+
const parsed = parseBootCommandArgs(args);
|
|
1697
|
+
validateBootCommandArgs(parsed);
|
|
1698
|
+
const imageOverride = parsed.positional[0];
|
|
1699
|
+
const paths = await resolveCliBaseAssets();
|
|
1700
|
+
const imagePath = imageOverride ? resolve(imageOverride) : paths.defaultImagePath;
|
|
1701
|
+
logBootPlan(paths, imagePath, parsed);
|
|
1702
|
+
const quiet = createBootQuietState(parsed, imageOverride);
|
|
1703
|
+
const vm = await startBootVm(parsed, paths, imagePath, bootEnvCommand(parsed), quiet);
|
|
1704
|
+
if (parsed.detached) {
|
|
1705
|
+
reportDetachedBoot(vm, parsed);
|
|
1706
|
+
return 0;
|
|
1707
|
+
}
|
|
1708
|
+
return runBootAttachedSession(vm, quiet);
|
|
1709
|
+
}
|
|
1710
|
+
function parseBootCommandArgs(args) {
|
|
1567
1711
|
try {
|
|
1568
|
-
|
|
1712
|
+
return parseRunArgs(args);
|
|
1569
1713
|
} catch (err) {
|
|
1570
1714
|
handleError(err);
|
|
1571
1715
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
);
|
|
1716
|
+
}
|
|
1717
|
+
function validateBootCommandArgs(parsed) {
|
|
1718
|
+
if (parsed.json && !parsed.detached) {
|
|
1719
|
+
die("boot --json is only meaningful with --detach (attached boots take over stdio).");
|
|
1577
1720
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
if (assetsOverride) {
|
|
1581
|
-
validateAssetsDir(assetsOverride);
|
|
1582
|
-
} else if (!baseAssetsComplete(RELEASE_TAG)) {
|
|
1583
|
-
process.stderr.write(`machinen: fetching base assets for ${RELEASE_TAG} (first run)
|
|
1584
|
-
`);
|
|
1585
|
-
await ensureBaseAssets(RELEASE_TAG);
|
|
1721
|
+
if (parsed.positional.length > 1) {
|
|
1722
|
+
die(bootUsage());
|
|
1586
1723
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1724
|
+
}
|
|
1725
|
+
function bootUsage() {
|
|
1726
|
+
return "usage: machinen boot [<image>] [--snapshot <path>] [--name <name>] [--cwd <abs-path>] [--mount ...] [--mount-live ...] [--env KEY=VALUE]... [--detached] [--nested] [--memory <mib>] [-- <cmd> [args...]]";
|
|
1727
|
+
}
|
|
1728
|
+
function logBootPlan(paths, imagePath, parsed) {
|
|
1729
|
+
debug(
|
|
1730
|
+
"boot baseDir=%s kernel=%s dtb=%s image=%s snapshot=%s name=%s",
|
|
1731
|
+
paths.baseDir,
|
|
1732
|
+
paths.kernelPath,
|
|
1733
|
+
paths.dtbPath,
|
|
1734
|
+
imagePath,
|
|
1735
|
+
parsed.snapshot ?? "<none>",
|
|
1736
|
+
parsed.name ?? "<unset>"
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
function bootEnvCommand(parsed) {
|
|
1740
|
+
if (parsed.double_dash_args.length === 0) {
|
|
1741
|
+
return void 0;
|
|
1596
1742
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1743
|
+
return ["/usr/bin/env", ...parsed.double_dash_args];
|
|
1744
|
+
}
|
|
1745
|
+
function createBootQuietState(parsed, imageOverride) {
|
|
1746
|
+
const headlineName = parsed.name ?? deriveBootName(imageOverride);
|
|
1747
|
+
const showHeadlines = shouldShowBootHeadlines(parsed);
|
|
1600
1748
|
const buffer = new RingBuffer();
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
printHeadline(`restoring ${headlineName}\u2026`);
|
|
1604
|
-
filter = new NoiseFilter({
|
|
1605
|
-
buffer,
|
|
1606
|
-
out: process.stderr,
|
|
1607
|
-
onReady: () => {
|
|
1608
|
-
printHeadline(`restored in ${formatElapsed(Date.now() - restoreT0)}`);
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1749
|
+
if (!showHeadlines) {
|
|
1750
|
+
return { headlineName, showHeadlines, buffer, filter: null, filterOut: null };
|
|
1611
1751
|
}
|
|
1612
|
-
|
|
1752
|
+
return createVisibleBootQuietState(parsed, headlineName, showHeadlines, buffer);
|
|
1753
|
+
}
|
|
1754
|
+
function shouldShowBootHeadlines(parsed) {
|
|
1755
|
+
if (!isQuiet()) {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
if (parsed.detached && parsed.json) {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
function createVisibleBootQuietState(parsed, headlineName, showHeadlines, buffer) {
|
|
1764
|
+
printHeadline(`booting ${headlineName}\u2026`);
|
|
1765
|
+
if (parsed.detached) {
|
|
1766
|
+
return bootBufferOnlyQuietState(headlineName, showHeadlines, buffer);
|
|
1767
|
+
}
|
|
1768
|
+
return bootFilteredQuietState(headlineName, showHeadlines, buffer, Date.now());
|
|
1769
|
+
}
|
|
1770
|
+
function bootBufferOnlyQuietState(headlineName, showHeadlines, buffer) {
|
|
1771
|
+
return {
|
|
1772
|
+
headlineName,
|
|
1773
|
+
showHeadlines,
|
|
1774
|
+
buffer,
|
|
1775
|
+
filter: null,
|
|
1776
|
+
filterOut: null,
|
|
1777
|
+
onLog: guestConsoleOnLog((chunk) => buffer.push(chunk))
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function bootFilteredQuietState(headlineName, showHeadlines, buffer, bootT0) {
|
|
1781
|
+
const filterOut = new PassThrough();
|
|
1782
|
+
const filter = new NoiseFilter({
|
|
1783
|
+
buffer,
|
|
1784
|
+
out: filterOut,
|
|
1785
|
+
onReady: () => {
|
|
1786
|
+
printHeadline("guest ready");
|
|
1787
|
+
printHeadline(`ready in ${formatElapsed(Date.now() - bootT0)}`);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
return {
|
|
1791
|
+
headlineName,
|
|
1792
|
+
showHeadlines,
|
|
1793
|
+
buffer,
|
|
1794
|
+
filter,
|
|
1795
|
+
filterOut,
|
|
1796
|
+
onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function guestConsoleOnLog(push) {
|
|
1800
|
+
return (evt) => {
|
|
1613
1801
|
if (evt.source === "guest-console") {
|
|
1614
|
-
|
|
1802
|
+
push(evt.chunk);
|
|
1615
1803
|
}
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
async function startBootVm(parsed, paths, imagePath, cmd, quiet) {
|
|
1618
1807
|
try {
|
|
1619
|
-
|
|
1620
|
-
|
|
1808
|
+
return await boot({
|
|
1809
|
+
// Always pass the base rootfs so /sbin/machinen-restore and
|
|
1810
|
+
// friends are in the initramfs even on a bare `machinen restore
|
|
1811
|
+
// <snap>` (no --image, no -- cmd).
|
|
1621
1812
|
image: imagePath,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1813
|
+
cmd,
|
|
1814
|
+
env: parsed.env,
|
|
1815
|
+
kernel: paths.kernelPath,
|
|
1816
|
+
dtb: paths.dtbPath,
|
|
1817
|
+
mount: parsed.mount,
|
|
1818
|
+
liveMounts: parsed.liveMounts,
|
|
1819
|
+
portForward: parsed.portForward,
|
|
1820
|
+
snapshot: parsed.snapshot,
|
|
1821
|
+
nested: parsed.nested,
|
|
1822
|
+
name: parsed.name,
|
|
1823
|
+
guestCwd: parsed.guestCwd,
|
|
1824
|
+
detached: parsed.detached,
|
|
1825
|
+
memory: parsed.memory,
|
|
1826
|
+
onLog: quiet.onLog,
|
|
1827
|
+
// Interactive CLI: the session lives as long as the guest does.
|
|
1828
|
+
// Don't impose the default 60s cap. Detached boots fall back to
|
|
1829
|
+
// the runtime's own readiness timeout (60s) so the CLI can't
|
|
1830
|
+
// hang forever waiting for first-guest-byte.
|
|
1831
|
+
timeoutMs: parsed.detached ? void 0 : null
|
|
1832
|
+
});
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
handleBootFailure(err, quiet);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function handleBootFailure(err, quiet) {
|
|
1838
|
+
quiet.filter?.flush();
|
|
1839
|
+
if (quiet.showHeadlines) {
|
|
1840
|
+
failQuiet(`boot ${quiet.headlineName} failed: ${describeError(err)}`, {
|
|
1841
|
+
buffer: quiet.buffer
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
handleError(err);
|
|
1845
|
+
}
|
|
1846
|
+
function reportDetachedBoot(vm, parsed) {
|
|
1847
|
+
if (parsed.json) {
|
|
1848
|
+
emitDetachedBootJson(vm, parsed);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
printDetachedBootHint(vm, parsed);
|
|
1852
|
+
}
|
|
1853
|
+
function emitDetachedBootJson(vm, parsed) {
|
|
1854
|
+
emitJson({ schema_version: 1, pid: vm.pid, name: parsed.name ?? null, detached: true });
|
|
1855
|
+
}
|
|
1856
|
+
function printDetachedBootHint(vm, parsed) {
|
|
1857
|
+
const target = parsed.name ?? `pid ${vm.pid}`;
|
|
1858
|
+
process.stderr.write(
|
|
1859
|
+
`machinen: detached (${target}). Reattach: machinen attach ${parsed.name ?? vm.pid}
|
|
1860
|
+
Stop: kill ${vm.pid} (machinen stop ships in PR2)
|
|
1861
|
+
`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
function runBootAttachedSession(vm, quiet) {
|
|
1865
|
+
return runAttachedVmSession(vm, {
|
|
1866
|
+
filter: quiet.filter,
|
|
1867
|
+
filterOut: quiet.filterOut,
|
|
1868
|
+
buffer: quiet.buffer,
|
|
1869
|
+
preReadyExitSummary: (code) => `boot ${quiet.headlineName} exited ${code} before reaching ready`
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
async function cmdInstall(args) {
|
|
1873
|
+
const opts = parseInstallOptions(args);
|
|
1874
|
+
const result = await installBaseAssets(opts);
|
|
1875
|
+
reportInstallResult(opts, result);
|
|
1876
|
+
return 0;
|
|
1877
|
+
}
|
|
1878
|
+
function parseInstallOptions(args) {
|
|
1879
|
+
const { json, rest } = consumeJsonFlag(args);
|
|
1880
|
+
return { json, tag: argValue(rest, "--version") ?? RELEASE_TAG };
|
|
1881
|
+
}
|
|
1882
|
+
async function installBaseAssets(opts) {
|
|
1883
|
+
const wasComplete = baseAssetsComplete(opts.tag);
|
|
1884
|
+
const t0 = Date.now();
|
|
1885
|
+
printInstallStart(opts);
|
|
1886
|
+
try {
|
|
1887
|
+
const base = await ensureBaseAssets(opts.tag);
|
|
1888
|
+
return { base, wasComplete, elapsedMs: Date.now() - t0 };
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
reportInstallFailure(opts, err);
|
|
1891
|
+
throw err;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function printInstallStart(opts) {
|
|
1895
|
+
if (opts.json) {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
process.stderr.write(`installing base assets for ${opts.tag}\u2026
|
|
1899
|
+
`);
|
|
1900
|
+
if (!isQuiet()) {
|
|
1901
|
+
process.stderr.write(` into ${cacheDirFor(opts.tag)}
|
|
1902
|
+
`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function reportInstallFailure(opts, err) {
|
|
1906
|
+
if (isQuiet() && !opts.json) {
|
|
1907
|
+
failQuiet(`install ${opts.tag} failed: ${describeError(err)}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function reportInstallResult(opts, result) {
|
|
1911
|
+
if (opts.json) {
|
|
1912
|
+
emitInstallJson(opts, result);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
printInstallReady(result);
|
|
1916
|
+
}
|
|
1917
|
+
function emitInstallJson(opts, result) {
|
|
1918
|
+
emitJson({
|
|
1919
|
+
schema_version: 1,
|
|
1920
|
+
tag: opts.tag,
|
|
1921
|
+
base_dir: result.base,
|
|
1922
|
+
fetched: !result.wasComplete
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function printInstallReady(result) {
|
|
1926
|
+
if (result.wasComplete) {
|
|
1927
|
+
process.stderr.write(`ready: ${result.base} (cached)
|
|
1928
|
+
`);
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
process.stderr.write(`ready in ${formatElapsed(result.elapsedMs)}: ${result.base}
|
|
1932
|
+
`);
|
|
1933
|
+
}
|
|
1934
|
+
async function cmdRestore(args) {
|
|
1935
|
+
const parsed = parseRestoreCommandArgs(args);
|
|
1936
|
+
validateRestoreCommandArgs(parsed);
|
|
1937
|
+
const snapDir = resolve(parsed.positional[0]);
|
|
1938
|
+
const paths = await resolveCliBaseAssets();
|
|
1939
|
+
const quiet = createRestoreQuietState(parsed, snapDir);
|
|
1940
|
+
const vm = await startRestoreVm(parsed, snapDir, paths, quiet);
|
|
1941
|
+
reportRestoreSuccess(vm, quiet);
|
|
1942
|
+
return runRestoreAttachedSession(vm, quiet);
|
|
1943
|
+
}
|
|
1944
|
+
function parseRestoreCommandArgs(args) {
|
|
1945
|
+
try {
|
|
1946
|
+
return parseRestoreArgs(args);
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
handleError(err);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function validateRestoreCommandArgs(parsed) {
|
|
1952
|
+
if (parsed.positional.length !== 1) {
|
|
1953
|
+
die(restoreUsage());
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function restoreUsage() {
|
|
1957
|
+
return "usage: machinen restore <snap-dir> [--image <tarball>] [--name <name>] [--lazy] [-p <hostPort>:<guestPort>] [--mount-live <host>:<guest>[:<mode>]]";
|
|
1958
|
+
}
|
|
1959
|
+
function createRestoreQuietState(parsed, snapDir) {
|
|
1960
|
+
const headlineName = parsed.name ?? deriveBootName(snapDir);
|
|
1961
|
+
const buffer = new RingBuffer();
|
|
1962
|
+
if (!isQuiet()) {
|
|
1963
|
+
return { headlineName, showHeadlines: false, buffer, filter: null, filterOut: null };
|
|
1964
|
+
}
|
|
1965
|
+
printHeadline(`restoring ${headlineName}\u2026`);
|
|
1966
|
+
return restoreFilteredQuietState(headlineName, buffer, Date.now());
|
|
1967
|
+
}
|
|
1968
|
+
function restoreFilteredQuietState(headlineName, buffer, restoreT0) {
|
|
1969
|
+
const filter = new NoiseFilter({
|
|
1970
|
+
buffer,
|
|
1971
|
+
out: process.stderr,
|
|
1972
|
+
onReady: () => {
|
|
1973
|
+
printHeadline(`restored in ${formatElapsed(Date.now() - restoreT0)}`);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
return {
|
|
1977
|
+
headlineName,
|
|
1978
|
+
showHeadlines: true,
|
|
1979
|
+
buffer,
|
|
1980
|
+
filter,
|
|
1981
|
+
filterOut: null,
|
|
1982
|
+
onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
async function startRestoreVm(parsed, snapDir, paths, quiet) {
|
|
1986
|
+
try {
|
|
1987
|
+
return await restore({
|
|
1988
|
+
snapDir,
|
|
1989
|
+
image: resolveOptionalImageOverride(parsed.image),
|
|
1990
|
+
kernel: paths.kernelPath,
|
|
1991
|
+
dtb: paths.dtbPath,
|
|
1992
|
+
name: parsed.name,
|
|
1993
|
+
lazy: parsed.lazy,
|
|
1994
|
+
portForward: optionalList(parsed.portForward),
|
|
1627
1995
|
// #273: per-guest overrides for the bundle's recorded
|
|
1628
1996
|
// liveMounts. Empty list = use the bundle's recorded mounts
|
|
1629
1997
|
// verbatim; non-empty entries replace the matching guest's
|
|
1630
1998
|
// host/mode (BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN if no match).
|
|
1631
|
-
liveMounts:
|
|
1999
|
+
liveMounts: optionalList(parsed.liveMounts),
|
|
1632
2000
|
timeoutMs: null,
|
|
1633
|
-
onLog
|
|
2001
|
+
onLog: quiet.onLog
|
|
1634
2002
|
});
|
|
1635
2003
|
} catch (err) {
|
|
1636
|
-
|
|
1637
|
-
if (showHeadlines) {
|
|
1638
|
-
failQuiet(`restore ${headlineName} failed: ${describeError(err)}`, {
|
|
1639
|
-
buffer
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
handleError(err);
|
|
2004
|
+
handleRestoreFailure(err, quiet);
|
|
1643
2005
|
}
|
|
1644
|
-
|
|
2006
|
+
}
|
|
2007
|
+
function optionalList(items) {
|
|
2008
|
+
if (items.length === 0) {
|
|
2009
|
+
return void 0;
|
|
2010
|
+
}
|
|
2011
|
+
return items;
|
|
2012
|
+
}
|
|
2013
|
+
function handleRestoreFailure(err, quiet) {
|
|
2014
|
+
quiet.filter?.flush();
|
|
2015
|
+
if (quiet.showHeadlines) {
|
|
2016
|
+
failQuiet(`restore ${quiet.headlineName} failed: ${describeError(err)}`, {
|
|
2017
|
+
buffer: quiet.buffer
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
handleError(err);
|
|
2021
|
+
}
|
|
2022
|
+
function reportRestoreSuccess(vm, quiet) {
|
|
2023
|
+
if (!quiet.showHeadlines) {
|
|
1645
2024
|
process.stderr.write(`restored as: ${vm.name ?? "<anonymous>"} (pid ${vm.pid})
|
|
1646
2025
|
`);
|
|
1647
2026
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
let forwardedSignal = null;
|
|
1655
|
-
const onSigint = () => {
|
|
1656
|
-
forwardedSignal = "SIGINT";
|
|
1657
|
-
void vm.kill();
|
|
1658
|
-
};
|
|
1659
|
-
const onSigterm = () => {
|
|
1660
|
-
forwardedSignal = "SIGTERM";
|
|
1661
|
-
void vm.kill();
|
|
1662
|
-
};
|
|
1663
|
-
process.on("SIGINT", onSigint);
|
|
1664
|
-
process.on("SIGTERM", onSigterm);
|
|
1665
|
-
pipeStdinToVm(vm.stdin, () => {
|
|
1666
|
-
process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
|
|
1667
|
-
forwardedSignal = "SIGTERM";
|
|
1668
|
-
void vm.kill();
|
|
2027
|
+
}
|
|
2028
|
+
function runRestoreAttachedSession(vm, quiet) {
|
|
2029
|
+
return runAttachedVmSession(vm, {
|
|
2030
|
+
filter: quiet.filter,
|
|
2031
|
+
buffer: quiet.buffer,
|
|
2032
|
+
preReadyExitSummary: (code) => `restore ${quiet.headlineName} exited ${code} before reaching ready`
|
|
1669
2033
|
});
|
|
1670
|
-
try {
|
|
1671
|
-
const { code } = await vm.wait();
|
|
1672
|
-
filter?.flush();
|
|
1673
|
-
if (forwardedSignal === "SIGINT") {
|
|
1674
|
-
return 130;
|
|
1675
|
-
}
|
|
1676
|
-
if (forwardedSignal === "SIGTERM") {
|
|
1677
|
-
return 143;
|
|
1678
|
-
}
|
|
1679
|
-
if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
|
|
1680
|
-
printDiagnostics(`restore ${headlineName} exited ${code} before reaching ready`, { buffer });
|
|
1681
|
-
}
|
|
1682
|
-
return code ?? 0;
|
|
1683
|
-
} finally {
|
|
1684
|
-
process.off("SIGINT", onSigint);
|
|
1685
|
-
process.off("SIGTERM", onSigterm);
|
|
1686
|
-
cancelHintRepeat();
|
|
1687
|
-
restoreStdin();
|
|
1688
|
-
}
|
|
1689
2034
|
}
|
|
1690
2035
|
async function cmdLs(args) {
|
|
1691
2036
|
const { json, rest } = consumeJsonFlag(args);
|
|
@@ -1693,52 +2038,95 @@ async function cmdLs(args) {
|
|
|
1693
2038
|
die(`unknown argument: ${rest[0]}`);
|
|
1694
2039
|
}
|
|
1695
2040
|
const entries = list();
|
|
1696
|
-
const rssByPid =
|
|
1697
|
-
entries.map((e) => ({ pid: e.pid, statsPath: e.statsPath }))
|
|
1698
|
-
);
|
|
2041
|
+
const rssByPid = rssByRegistryPid(entries);
|
|
1699
2042
|
if (json) {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2043
|
+
emitLsJson(entries, rssByPid);
|
|
2044
|
+
} else {
|
|
2045
|
+
printLsTable(entries, rssByPid);
|
|
2046
|
+
}
|
|
2047
|
+
return 0;
|
|
2048
|
+
}
|
|
2049
|
+
function rssByRegistryPid(entries) {
|
|
2050
|
+
return readHostRssBytesMulti(
|
|
2051
|
+
entries.map((entry) => ({ pid: entry.pid, statsPath: entry.statsPath }))
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
function emitLsJson(entries, rssByPid) {
|
|
2055
|
+
emitJson({
|
|
2056
|
+
schema_version: 1,
|
|
2057
|
+
vms: entries.map((entry) => vmJson(entry, rssByPid))
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function vmJson(entry, rssByPid) {
|
|
2061
|
+
return {
|
|
2062
|
+
pid: entry.pid,
|
|
2063
|
+
name: nullable(entry.name),
|
|
2064
|
+
started_at: entry.startedAt,
|
|
2065
|
+
uptime_ms: Date.now() - entry.startedAt,
|
|
2066
|
+
memory: vmMemoryJson(entry, rssByPid),
|
|
2067
|
+
ports: portsJson(entry),
|
|
2068
|
+
forked_from: nullable(entry.forkedFrom)
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function portsJson(entry) {
|
|
2072
|
+
if (entry.portForward === void 0) {
|
|
2073
|
+
return [];
|
|
1716
2074
|
}
|
|
2075
|
+
return entry.portForward;
|
|
2076
|
+
}
|
|
2077
|
+
function vmMemoryJson(entry, rssByPid) {
|
|
2078
|
+
return {
|
|
2079
|
+
rss_bytes: nullable(rssByPid.get(entry.pid)),
|
|
2080
|
+
ceiling_mib: nullable(entry.memoryCeilingMib)
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function nullable(value) {
|
|
2084
|
+
if (value === void 0) {
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
return value;
|
|
2088
|
+
}
|
|
2089
|
+
function printLsTable(entries, rssByPid) {
|
|
1717
2090
|
if (entries.length === 0) {
|
|
1718
2091
|
process.stdout.write("(no running VMs)\n");
|
|
1719
|
-
return
|
|
2092
|
+
return;
|
|
1720
2093
|
}
|
|
1721
2094
|
const header = ["PID", "NAME", "UP", "MEM", "PORTS", "FORKED-FROM"];
|
|
1722
|
-
const rows = entries
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2095
|
+
const rows = lsRows(entries, rssByPid);
|
|
2096
|
+
const widths = tableWidths(header, rows);
|
|
2097
|
+
const visible = visibleLsColumns(header, widths);
|
|
2098
|
+
printTable(header, rows, widths, visible);
|
|
2099
|
+
}
|
|
2100
|
+
function lsRows(entries, rssByPid) {
|
|
2101
|
+
return entries.map((entry) => [
|
|
2102
|
+
String(entry.pid),
|
|
2103
|
+
entry.name ?? "-",
|
|
2104
|
+
formatUptime(Date.now() - entry.startedAt),
|
|
2105
|
+
formatMem(rssByPid.get(entry.pid) ?? null, entry.memoryCeilingMib),
|
|
2106
|
+
formatPorts(entry.portForward),
|
|
2107
|
+
entry.forkedFrom ?? "-"
|
|
1729
2108
|
]);
|
|
1730
|
-
|
|
2109
|
+
}
|
|
2110
|
+
function tableWidths(header, rows) {
|
|
2111
|
+
return header.map(
|
|
2112
|
+
(heading, index) => Math.max(heading.length, ...rows.map((row) => row[index].length))
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
function visibleLsColumns(header, widths) {
|
|
1731
2116
|
const gap = " ";
|
|
1732
|
-
const fullWidth = widths.reduce((sum,
|
|
2117
|
+
const fullWidth = widths.reduce((sum, width) => sum + width, 0) + gap.length * (widths.length - 1);
|
|
1733
2118
|
const cols = process.stdout.columns;
|
|
1734
2119
|
const includeMem = cols === void 0 || fullWidth <= cols;
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
2120
|
+
return includeMem ? header.map((_, i) => i) : header.map((_, i) => i).filter((i) => i !== 3);
|
|
2121
|
+
}
|
|
2122
|
+
function printTable(header, rows, widths, visible) {
|
|
2123
|
+
process.stdout.write(formatTableLine(header, widths, visible) + "\n");
|
|
1738
2124
|
for (const row of rows) {
|
|
1739
|
-
process.stdout.write(
|
|
2125
|
+
process.stdout.write(formatTableLine(row, widths, visible) + "\n");
|
|
1740
2126
|
}
|
|
1741
|
-
|
|
2127
|
+
}
|
|
2128
|
+
function formatTableLine(cells, widths, visible) {
|
|
2129
|
+
return visible.map((index) => cells[index].padEnd(widths[index])).join(" ");
|
|
1742
2130
|
}
|
|
1743
2131
|
function formatUptime(ms) {
|
|
1744
2132
|
const s = Math.floor(ms / 1e3);
|
|
@@ -1756,180 +2144,265 @@ function formatUptime(ms) {
|
|
|
1756
2144
|
return `${Math.floor(h / 24)}d`;
|
|
1757
2145
|
}
|
|
1758
2146
|
async function cmdGc(args) {
|
|
1759
|
-
const { json, rest
|
|
1760
|
-
|
|
1761
|
-
for (const a of rest) {
|
|
1762
|
-
die(`unknown flag: ${a}`);
|
|
1763
|
-
}
|
|
2147
|
+
const { json, dryRun, rest } = parseGcOptions(args);
|
|
2148
|
+
dieOnUnexpectedArgs(rest);
|
|
1764
2149
|
const results = runGc({ dryRun });
|
|
1765
2150
|
if (json) {
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
results: results.map((r) => ({
|
|
1770
|
-
pid: r.pid,
|
|
1771
|
-
name: r.name ?? null,
|
|
1772
|
-
status: r.status,
|
|
1773
|
-
removed_paths: r.removedPaths,
|
|
1774
|
-
failed_paths: r.failedPaths
|
|
1775
|
-
}))
|
|
1776
|
-
});
|
|
1777
|
-
return 0;
|
|
2151
|
+
emitGcJson(dryRun, results);
|
|
2152
|
+
} else {
|
|
2153
|
+
printGcResults(results, dryRun);
|
|
1778
2154
|
}
|
|
2155
|
+
return 0;
|
|
2156
|
+
}
|
|
2157
|
+
function parseGcOptions(args) {
|
|
2158
|
+
const { json, rest: afterJson } = consumeJsonFlag(args);
|
|
2159
|
+
const { dryRun, rest } = consumeDryRunFlag(afterJson);
|
|
2160
|
+
return { json, dryRun, rest };
|
|
2161
|
+
}
|
|
2162
|
+
function dieOnUnexpectedArgs(args) {
|
|
2163
|
+
for (const arg of args) {
|
|
2164
|
+
die(`unknown flag: ${arg}`);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function emitGcJson(dryRun, results) {
|
|
2168
|
+
emitJson({
|
|
2169
|
+
schema_version: 1,
|
|
2170
|
+
dry_run: dryRun,
|
|
2171
|
+
results: results.map((r) => ({
|
|
2172
|
+
pid: r.pid,
|
|
2173
|
+
name: r.name ?? null,
|
|
2174
|
+
status: r.status,
|
|
2175
|
+
removed_paths: r.removedPaths,
|
|
2176
|
+
failed_paths: r.failedPaths
|
|
2177
|
+
}))
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
function printGcResults(results, dryRun) {
|
|
1779
2181
|
if (results.length === 0) {
|
|
1780
2182
|
process.stdout.write("(nothing to clean up)\n");
|
|
1781
|
-
return
|
|
2183
|
+
return;
|
|
1782
2184
|
}
|
|
1783
|
-
for (const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
|
|
2185
|
+
for (const result of results) {
|
|
2186
|
+
printGcResult(result, dryRun);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
function printGcResult(result, dryRun) {
|
|
2190
|
+
const label = result.name ? `${result.name} (pid ${result.pid})` : `pid ${result.pid}`;
|
|
2191
|
+
const verb = dryRun ? "would clean" : "cleaned";
|
|
2192
|
+
process.stdout.write(
|
|
2193
|
+
`${verb} ${label} [${result.status}]: ${result.removedPaths.length} path(s)
|
|
2194
|
+
`
|
|
2195
|
+
);
|
|
2196
|
+
printIndentedPaths(result.removedPaths, "");
|
|
2197
|
+
printIndentedPaths(result.failedPaths, "failed: ");
|
|
2198
|
+
}
|
|
2199
|
+
function printIndentedPaths(paths, prefix) {
|
|
2200
|
+
for (const path of paths) {
|
|
2201
|
+
process.stdout.write(` ${prefix}${path}
|
|
1794
2202
|
`);
|
|
1795
|
-
}
|
|
1796
2203
|
}
|
|
1797
|
-
return 0;
|
|
1798
2204
|
}
|
|
1799
2205
|
async function cmdStop(args) {
|
|
2206
|
+
const opts = parseStopOptions(args);
|
|
2207
|
+
const entry = lookupEntry(opts.target);
|
|
2208
|
+
if (!entry) {
|
|
2209
|
+
reportStopMissingTarget(opts);
|
|
2210
|
+
return 1;
|
|
2211
|
+
}
|
|
2212
|
+
return stopExistingEntry(entry, opts);
|
|
2213
|
+
}
|
|
2214
|
+
async function stopExistingEntry(entry, opts) {
|
|
2215
|
+
const status = validateStopEntry(entry);
|
|
2216
|
+
if (await handleInactiveStopEntry(entry, status, opts)) {
|
|
2217
|
+
return 0;
|
|
2218
|
+
}
|
|
2219
|
+
if (opts.dryRun) {
|
|
2220
|
+
reportStopDryRun(entry, opts);
|
|
2221
|
+
return 0;
|
|
2222
|
+
}
|
|
2223
|
+
return stopLiveEntry(entry, opts);
|
|
2224
|
+
}
|
|
2225
|
+
async function stopLiveEntry(entry, opts) {
|
|
2226
|
+
const sig = stopSignal(opts.force);
|
|
2227
|
+
if (!signalStopProcess(entry.pid, sig, opts, "STOP_KILL_FAILED")) {
|
|
2228
|
+
return 1;
|
|
2229
|
+
}
|
|
2230
|
+
await escalateIfNeeded(entry.pid, opts.force);
|
|
2231
|
+
await stopGvproxy(entry, sig, opts.force);
|
|
2232
|
+
finishStoppedEntry(entry, opts);
|
|
2233
|
+
return 0;
|
|
2234
|
+
}
|
|
2235
|
+
function parseStopOptions(args) {
|
|
1800
2236
|
const { json, rest: afterJson } = consumeJsonFlag(args);
|
|
1801
2237
|
const { dryRun, rest: afterDry } = consumeDryRunFlag(afterJson);
|
|
1802
|
-
|
|
2238
|
+
const { force, rest } = consumeForceFlag(afterDry);
|
|
2239
|
+
return { json, dryRun, force, target: parseTargetFlags(rest, "stop") };
|
|
2240
|
+
}
|
|
2241
|
+
function consumeForceFlag(args) {
|
|
1803
2242
|
const rest = [];
|
|
1804
|
-
|
|
1805
|
-
|
|
2243
|
+
let force = false;
|
|
2244
|
+
for (const arg of args) {
|
|
2245
|
+
if (arg === "--force" || arg === "-9") {
|
|
1806
2246
|
force = true;
|
|
1807
2247
|
} else {
|
|
1808
|
-
rest.push(
|
|
2248
|
+
rest.push(arg);
|
|
1809
2249
|
}
|
|
1810
2250
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2251
|
+
return { force, rest };
|
|
2252
|
+
}
|
|
2253
|
+
function reportStopMissingTarget(opts) {
|
|
2254
|
+
const message = `no running VM matched ${describeTarget(opts.target)}`;
|
|
2255
|
+
if (opts.json) {
|
|
2256
|
+
emitJsonError("VM_NOT_FOUND", message);
|
|
2257
|
+
} else {
|
|
2258
|
+
process.stderr.write(`machinen stop: ${message}
|
|
1818
2259
|
`);
|
|
1819
|
-
}
|
|
1820
|
-
return 1;
|
|
1821
2260
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2261
|
+
}
|
|
2262
|
+
function emitStop(entry, opts, status) {
|
|
2263
|
+
if (!opts.json) {
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
emitJson({
|
|
2267
|
+
schema_version: 1,
|
|
2268
|
+
pid: entry.pid,
|
|
2269
|
+
name: entry.name ?? null,
|
|
2270
|
+
status,
|
|
2271
|
+
dry_run: opts.dryRun
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
function validateStopEntry(entry) {
|
|
2275
|
+
return validatePid(entry.pid, {
|
|
1834
2276
|
vmmExe: entry.vmmExe,
|
|
1835
2277
|
startedAt: entry.startedAt
|
|
1836
2278
|
});
|
|
2279
|
+
}
|
|
2280
|
+
async function handleInactiveStopEntry(entry, status, opts) {
|
|
1837
2281
|
if (status === "recycled") {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
if (!dryRun) {
|
|
1844
|
-
runGc({ pid: entry.pid });
|
|
1845
|
-
}
|
|
1846
|
-
emitStop("recycled");
|
|
1847
|
-
return 0;
|
|
2282
|
+
reportRecycledStopEntry(entry, opts);
|
|
2283
|
+
gcStoppedEntry(entry, opts.dryRun);
|
|
2284
|
+
emitStop(entry, opts, "recycled");
|
|
2285
|
+
return true;
|
|
1848
2286
|
}
|
|
1849
2287
|
if (status === "dead") {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
}
|
|
1855
|
-
if (!dryRun) {
|
|
1856
|
-
runGc({ pid: entry.pid });
|
|
1857
|
-
}
|
|
1858
|
-
emitStop("already_dead");
|
|
1859
|
-
return 0;
|
|
2288
|
+
reportDeadStopEntry(entry, opts);
|
|
2289
|
+
gcStoppedEntry(entry, opts.dryRun);
|
|
2290
|
+
emitStop(entry, opts, "already_dead");
|
|
2291
|
+
return true;
|
|
1860
2292
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
function reportRecycledStopEntry(entry, opts) {
|
|
2296
|
+
if (opts.json) {
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
process.stderr.write(
|
|
2300
|
+
`machinen stop: registry entry pid ${entry.pid} is now held by an unrelated process; ` + (opts.dryRun ? "would skip kill and gc.\n" : "skipping kill and running gc.\n")
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
function reportDeadStopEntry(entry, opts) {
|
|
2304
|
+
if (opts.json) {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
process.stderr.write(
|
|
2308
|
+
`machinen stop: pid ${entry.pid} already gone; ` + (opts.dryRun ? "would gc.\n" : "running gc.\n")
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
function gcStoppedEntry(entry, dryRun) {
|
|
2312
|
+
if (!dryRun) {
|
|
2313
|
+
runGc({ pid: entry.pid });
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function reportStopDryRun(entry, opts) {
|
|
2317
|
+
if (!opts.json) {
|
|
2318
|
+
const sigLabel = opts.force ? "SIGKILL" : "SIGTERM (escalates to SIGKILL after 2s)";
|
|
2319
|
+
process.stdout.write(`would ${sigLabel} ${entryLabel(entry)}
|
|
1866
2320
|
`);
|
|
1867
|
-
}
|
|
1868
|
-
emitStop("would_stop");
|
|
1869
|
-
return 0;
|
|
1870
2321
|
}
|
|
1871
|
-
|
|
2322
|
+
emitStop(entry, opts, "would_stop");
|
|
2323
|
+
}
|
|
2324
|
+
function stopSignal(force) {
|
|
2325
|
+
return force ? "SIGKILL" : "SIGTERM";
|
|
2326
|
+
}
|
|
2327
|
+
function signalStopProcess(pid, signal, opts, errorCode) {
|
|
1872
2328
|
try {
|
|
1873
|
-
process.kill(
|
|
2329
|
+
process.kill(pid, signal);
|
|
2330
|
+
return true;
|
|
1874
2331
|
} catch (err) {
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2332
|
+
reportStopSignalError(pid, err, opts, errorCode);
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function reportStopSignalError(pid, err, opts, errorCode) {
|
|
2337
|
+
const msg = `failed to signal pid ${pid}: ${describeError(err)}`;
|
|
2338
|
+
if (opts.json) {
|
|
2339
|
+
emitJsonError(errorCode, msg);
|
|
2340
|
+
} else {
|
|
2341
|
+
process.stderr.write(`machinen stop: ${msg}
|
|
1880
2342
|
`);
|
|
1881
|
-
}
|
|
1882
|
-
return 1;
|
|
1883
2343
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
try {
|
|
1889
|
-
process.kill(entry.pid, "SIGKILL");
|
|
1890
|
-
} catch {
|
|
1891
|
-
}
|
|
1892
|
-
} catch {
|
|
1893
|
-
}
|
|
2344
|
+
}
|
|
2345
|
+
async function escalateIfNeeded(pid, force) {
|
|
2346
|
+
if (force) {
|
|
2347
|
+
return;
|
|
1894
2348
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2349
|
+
await waitForExit(pid, 2e3);
|
|
2350
|
+
if (pidIsAlive(pid)) {
|
|
2351
|
+
tryKill(pid, "SIGKILL");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
function pidIsAlive(pid) {
|
|
2355
|
+
try {
|
|
2356
|
+
process.kill(pid, 0);
|
|
2357
|
+
return true;
|
|
2358
|
+
} catch {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function tryKill(pid, signal) {
|
|
2363
|
+
try {
|
|
2364
|
+
process.kill(pid, signal);
|
|
2365
|
+
} catch {
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
async function stopGvproxy(entry, signal, force) {
|
|
2369
|
+
if (!entry.gvproxyPid || !entry.gvproxyExe) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
await handleGvproxyStatus(
|
|
2373
|
+
entry.gvproxyPid,
|
|
2374
|
+
validatePid(entry.gvproxyPid, { vmmExe: entry.gvproxyExe }),
|
|
2375
|
+
signal,
|
|
2376
|
+
force
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
async function handleGvproxyStatus(pid, status, signal, force) {
|
|
2380
|
+
if (status === "alive") {
|
|
2381
|
+
await signalGvproxy(pid, signal, force);
|
|
2382
|
+
} else if (status === "recycled") {
|
|
2383
|
+
process.stderr.write(
|
|
2384
|
+
`machinen stop: gvproxy pid ${pid} now held by an unrelated process; skipping.
|
|
1920
2385
|
`
|
|
1921
|
-
|
|
1922
|
-
}
|
|
2386
|
+
);
|
|
1923
2387
|
}
|
|
2388
|
+
}
|
|
2389
|
+
async function signalGvproxy(pid, signal, force) {
|
|
2390
|
+
if (!signalStopProcess(pid, signal, { json: false }, "STOP_GVPROXY_KILL_FAILED")) {
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
await escalateIfNeeded(pid, force);
|
|
2394
|
+
}
|
|
2395
|
+
function finishStoppedEntry(entry, opts) {
|
|
1924
2396
|
runGc({ pid: entry.pid });
|
|
1925
|
-
if (json) {
|
|
1926
|
-
emitStop("stopped");
|
|
2397
|
+
if (opts.json) {
|
|
2398
|
+
emitStop(entry, opts, "stopped");
|
|
1927
2399
|
} else {
|
|
1928
|
-
|
|
1929
|
-
process.stdout.write(`stopped ${label}
|
|
2400
|
+
process.stdout.write(`stopped ${entryLabel(entry)}
|
|
1930
2401
|
`);
|
|
1931
2402
|
}
|
|
1932
|
-
|
|
2403
|
+
}
|
|
2404
|
+
function entryLabel(entry) {
|
|
2405
|
+
return entry.name ? `${entry.name} (pid ${entry.pid})` : `pid ${entry.pid}`;
|
|
1933
2406
|
}
|
|
1934
2407
|
async function waitForExit(pid, timeoutMs) {
|
|
1935
2408
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1943,406 +2416,510 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
1943
2416
|
}
|
|
1944
2417
|
}
|
|
1945
2418
|
function lookupEntry(target) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return e;
|
|
1952
|
-
}
|
|
2419
|
+
return list().find((entry) => entryMatchesTarget(entry, target));
|
|
2420
|
+
}
|
|
2421
|
+
function entryMatchesTarget(entry, target) {
|
|
2422
|
+
if ("name" in target) {
|
|
2423
|
+
return entry.name === target.name;
|
|
1953
2424
|
}
|
|
1954
|
-
return
|
|
2425
|
+
return entry.pid === target.pid;
|
|
1955
2426
|
}
|
|
1956
2427
|
function describeTarget(target) {
|
|
1957
2428
|
return "name" in target ? `name ${target.name}` : `pid ${target.pid}`;
|
|
1958
2429
|
}
|
|
1959
2430
|
async function cmdExec(args) {
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
filtered.push(a);
|
|
1967
|
-
}
|
|
2431
|
+
const parsed = parseExecArgs(args);
|
|
2432
|
+
const vm = await attach(parsed.target).catch(handleError);
|
|
2433
|
+
try {
|
|
2434
|
+
return await runExecCommand(vm, parsed);
|
|
2435
|
+
} finally {
|
|
2436
|
+
await vm.detach();
|
|
1968
2437
|
}
|
|
2438
|
+
}
|
|
2439
|
+
function parseExecArgs(args) {
|
|
2440
|
+
const { usePty, filtered } = consumeExecPtyFlag(args);
|
|
1969
2441
|
const dashIdx = filtered.indexOf("--");
|
|
1970
2442
|
if (dashIdx === -1 || dashIdx === filtered.length - 1) {
|
|
1971
2443
|
die("usage: machinen exec <name|pid> [--tty] -- <cmd>");
|
|
1972
2444
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2445
|
+
return {
|
|
2446
|
+
usePty,
|
|
2447
|
+
target: parseTargetFlags(filtered.slice(0, dashIdx), "exec"),
|
|
2448
|
+
cmd: filtered.slice(dashIdx + 1).join(" ")
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function consumeExecPtyFlag(args) {
|
|
2452
|
+
const filtered = [];
|
|
2453
|
+
let usePty = false;
|
|
2454
|
+
for (const arg of args) {
|
|
2455
|
+
if (arg === "--tty" || arg === "--pty") {
|
|
2456
|
+
usePty = true;
|
|
2457
|
+
} else {
|
|
2458
|
+
filtered.push(arg);
|
|
1984
2459
|
}
|
|
1985
|
-
const res = await vm.execRaw(joined, {
|
|
1986
|
-
onStdout: (chunk) => process.stdout.write(chunk),
|
|
1987
|
-
onStderr: (chunk) => process.stderr.write(chunk)
|
|
1988
|
-
});
|
|
1989
|
-
return res.exitCode;
|
|
1990
|
-
} finally {
|
|
1991
|
-
await vm.detach();
|
|
1992
2460
|
}
|
|
2461
|
+
return { usePty, filtered };
|
|
2462
|
+
}
|
|
2463
|
+
async function runExecCommand(vm, parsed) {
|
|
2464
|
+
if (parsed.usePty) {
|
|
2465
|
+
assertExecPtyTty();
|
|
2466
|
+
return runPtyExec(vm, parsed.cmd);
|
|
2467
|
+
}
|
|
2468
|
+
return runRawExec(vm, parsed.cmd);
|
|
2469
|
+
}
|
|
2470
|
+
function assertExecPtyTty() {
|
|
2471
|
+
if (!process.stdin.isTTY) {
|
|
2472
|
+
die("machinen exec --tty: stdin is not a TTY; pass via terminal or drop --tty");
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
async function runRawExec(vm, cmd) {
|
|
2476
|
+
const res = await vm.execRaw(cmd, {
|
|
2477
|
+
onStdout: (chunk) => process.stdout.write(chunk),
|
|
2478
|
+
onStderr: (chunk) => process.stderr.write(chunk)
|
|
2479
|
+
});
|
|
2480
|
+
return res.exitCode;
|
|
1993
2481
|
}
|
|
1994
2482
|
async function runPtyExec(vm, cmd) {
|
|
1995
|
-
const
|
|
1996
|
-
const stdout = process.stdout;
|
|
1997
|
-
const initialCols = stdout.columns ?? 80;
|
|
1998
|
-
const initialRows = stdout.rows ?? 24;
|
|
1999
|
-
const wasRaw = stdin.isRaw === true;
|
|
2000
|
-
stdin.setRawMode(true);
|
|
2001
|
-
stdin.resume();
|
|
2483
|
+
const tty = enterPtyRawMode();
|
|
2002
2484
|
const handle = vm.execPty(cmd, {
|
|
2003
|
-
cols:
|
|
2004
|
-
rows:
|
|
2005
|
-
stdin,
|
|
2006
|
-
stdout
|
|
2485
|
+
cols: tty.cols,
|
|
2486
|
+
rows: tty.rows,
|
|
2487
|
+
stdin: process.stdin,
|
|
2488
|
+
stdout: process.stdout
|
|
2007
2489
|
});
|
|
2008
|
-
const onResize = () =>
|
|
2009
|
-
|
|
2010
|
-
};
|
|
2011
|
-
stdout.on("resize", onResize);
|
|
2490
|
+
const onResize = () => handle.resize(process.stdout.columns ?? tty.cols, process.stdout.rows ?? tty.rows);
|
|
2491
|
+
process.stdout.on("resize", onResize);
|
|
2012
2492
|
try {
|
|
2013
2493
|
const { exitCode } = await handle.result;
|
|
2014
2494
|
return exitCode;
|
|
2015
2495
|
} finally {
|
|
2016
|
-
stdout.removeListener("resize", onResize);
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2496
|
+
process.stdout.removeListener("resize", onResize);
|
|
2497
|
+
tty.restore();
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
function enterPtyRawMode() {
|
|
2501
|
+
const wasRaw = process.stdin.isRaw === true;
|
|
2502
|
+
process.stdin.setRawMode(true);
|
|
2503
|
+
process.stdin.resume();
|
|
2504
|
+
return {
|
|
2505
|
+
cols: process.stdout.columns ?? 80,
|
|
2506
|
+
rows: process.stdout.rows ?? 24,
|
|
2507
|
+
restore: () => restorePtyRawMode(wasRaw)
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
function restorePtyRawMode(wasRaw) {
|
|
2511
|
+
if (wasRaw) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
process.stdin.setRawMode(false);
|
|
2516
|
+
} catch {
|
|
2023
2517
|
}
|
|
2024
2518
|
}
|
|
2025
2519
|
async function cmdSnapshot(args) {
|
|
2520
|
+
const opts = parseSnapshotOptions(args);
|
|
2521
|
+
if (opts.dryRun) {
|
|
2522
|
+
return snapshotDryRun(opts);
|
|
2523
|
+
}
|
|
2524
|
+
return runSnapshot(opts);
|
|
2525
|
+
}
|
|
2526
|
+
function parseSnapshotOptions(args) {
|
|
2026
2527
|
const { json, rest: afterJson } = consumeJsonFlag(args);
|
|
2027
2528
|
const { dryRun, rest: afterDry } = consumeDryRunFlag(afterJson);
|
|
2529
|
+
const { keepAlive, rest } = consumeKeepAliveFlag(afterDry);
|
|
2530
|
+
const { target, rest: afterTarget } = resolveTarget(rest, "snapshot");
|
|
2531
|
+
const outDir = parseSnapshotOutDir(afterTarget);
|
|
2532
|
+
return { json, dryRun, keepAlive, target, outDir, resolvedOutDir: resolve(outDir) };
|
|
2533
|
+
}
|
|
2534
|
+
function consumeKeepAliveFlag(args) {
|
|
2535
|
+
const rest = [];
|
|
2028
2536
|
let keepAlive = false;
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
if (a === "--keep-alive") {
|
|
2537
|
+
for (const arg of args) {
|
|
2538
|
+
if (arg === "--keep-alive") {
|
|
2032
2539
|
keepAlive = true;
|
|
2033
2540
|
} else {
|
|
2034
|
-
|
|
2541
|
+
rest.push(arg);
|
|
2035
2542
|
}
|
|
2036
2543
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2544
|
+
return { keepAlive, rest };
|
|
2545
|
+
}
|
|
2546
|
+
function parseSnapshotOutDir(args) {
|
|
2547
|
+
if (args.length === 0) {
|
|
2039
2548
|
die("usage: machinen snapshot <name|pid> <out-dir> [--keep-alive] [--dry-run] [--json]");
|
|
2040
2549
|
}
|
|
2041
|
-
if (
|
|
2042
|
-
die(`unknown argument: ${
|
|
2550
|
+
if (args.length > 1) {
|
|
2551
|
+
die(`unknown argument: ${args[1]}`);
|
|
2552
|
+
}
|
|
2553
|
+
return args[0];
|
|
2554
|
+
}
|
|
2555
|
+
function snapshotDryRun(opts) {
|
|
2556
|
+
const entry = lookupEntry(opts.target);
|
|
2557
|
+
if (!entry) {
|
|
2558
|
+
reportSnapshotMissingTarget(opts);
|
|
2559
|
+
return 1;
|
|
2560
|
+
}
|
|
2561
|
+
reportSnapshotDryRun(entry, opts);
|
|
2562
|
+
return 0;
|
|
2563
|
+
}
|
|
2564
|
+
function reportSnapshotMissingTarget(opts) {
|
|
2565
|
+
const msg = `no running VM matched ${describeTarget(opts.target)}`;
|
|
2566
|
+
if (opts.json) {
|
|
2567
|
+
emitJsonError("VM_NOT_FOUND", msg);
|
|
2568
|
+
} else {
|
|
2569
|
+
process.stderr.write(`machinen snapshot: ${msg}
|
|
2570
|
+
`);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
function reportSnapshotDryRun(entry, opts) {
|
|
2574
|
+
if (opts.json) {
|
|
2575
|
+
emitSnapshotJson(opts.resolvedOutDir, 0, true);
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
const suffix = opts.keepAlive ? " (--keep-alive)\n" : "\n";
|
|
2579
|
+
process.stdout.write(`would snapshot ${entryLabel(entry)} \u2192 ${opts.resolvedOutDir}${suffix}`);
|
|
2580
|
+
}
|
|
2581
|
+
async function runSnapshot(opts) {
|
|
2582
|
+
const vm = await attach(opts.target).catch(handleError);
|
|
2583
|
+
const quiet = createSnapshotQuietState(vm, opts);
|
|
2584
|
+
try {
|
|
2585
|
+
const res = await vm.snapshot({
|
|
2586
|
+
outDir: opts.resolvedOutDir,
|
|
2587
|
+
leaveRunning: opts.keepAlive,
|
|
2588
|
+
tcpClose: opts.keepAlive,
|
|
2589
|
+
onLog: snapshotOnLog(quiet)
|
|
2590
|
+
});
|
|
2591
|
+
reportSnapshotSuccess(res.snapDir, res.elapsedMs, opts);
|
|
2592
|
+
return 0;
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
handleSnapshotFailure(err, quiet);
|
|
2595
|
+
} finally {
|
|
2596
|
+
await vm.detach();
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
function createSnapshotQuietState(vm, opts) {
|
|
2600
|
+
const headlineName = vm.name ?? `pid ${vm.pid}`;
|
|
2601
|
+
const showHeadlines = isQuiet() && !opts.json;
|
|
2602
|
+
const buffer = new RingBuffer();
|
|
2603
|
+
if (showHeadlines) {
|
|
2604
|
+
printHeadline(`snapshotting ${headlineName}\u2026`);
|
|
2043
2605
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
if (
|
|
2049
|
-
|
|
2050
|
-
if (json) {
|
|
2051
|
-
emitJsonError("VM_NOT_FOUND", msg);
|
|
2052
|
-
} else {
|
|
2053
|
-
process.stderr.write(`machinen snapshot: ${msg}
|
|
2054
|
-
`);
|
|
2055
|
-
}
|
|
2056
|
-
return 1;
|
|
2606
|
+
return { headlineName, showHeadlines, buffer, filter: null, filterOut: null };
|
|
2607
|
+
}
|
|
2608
|
+
function snapshotOnLog(quiet) {
|
|
2609
|
+
return (evt) => {
|
|
2610
|
+
if (evt.source === "phase") {
|
|
2611
|
+
return;
|
|
2057
2612
|
}
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2060
|
-
schema_version: 1,
|
|
2061
|
-
snap_dir: resolvedOutDir,
|
|
2062
|
-
elapsed_ms: 0,
|
|
2063
|
-
dry_run: true
|
|
2064
|
-
});
|
|
2613
|
+
if (quiet.showHeadlines) {
|
|
2614
|
+
quiet.buffer.push(evt.chunk);
|
|
2065
2615
|
} else {
|
|
2066
|
-
|
|
2067
|
-
process.stdout.write(
|
|
2068
|
-
`would snapshot ${label} \u2192 ${resolvedOutDir}` + (keepAlive ? " (--keep-alive)\n" : "\n")
|
|
2069
|
-
);
|
|
2616
|
+
process.stderr.write(evt.chunk);
|
|
2070
2617
|
}
|
|
2071
|
-
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
function reportSnapshotSuccess(snapDir, elapsedMs, opts) {
|
|
2621
|
+
if (opts.json) {
|
|
2622
|
+
emitSnapshotJson(snapDir, elapsedMs, false);
|
|
2623
|
+
return;
|
|
2072
2624
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2625
|
+
process.stdout.write(`snapshot: ${snapDir} (${elapsedMs}ms)
|
|
2626
|
+
`);
|
|
2627
|
+
}
|
|
2628
|
+
function emitSnapshotJson(snapDir, elapsedMs, dryRun) {
|
|
2629
|
+
emitJson({ schema_version: 1, snap_dir: snapDir, elapsed_ms: elapsedMs, dry_run: dryRun });
|
|
2630
|
+
}
|
|
2631
|
+
function handleSnapshotFailure(err, quiet) {
|
|
2632
|
+
if (quiet.showHeadlines) {
|
|
2633
|
+
failQuiet(`snapshot ${quiet.headlineName} failed: ${describeError(err)}`, {
|
|
2634
|
+
buffer: quiet.buffer
|
|
2635
|
+
});
|
|
2079
2636
|
}
|
|
2637
|
+
handleError(err);
|
|
2638
|
+
}
|
|
2639
|
+
async function cmdFork(args) {
|
|
2640
|
+
const opts = await prepareForkCommand(args);
|
|
2641
|
+
const vm = await attach(opts.target).catch(handleError);
|
|
2080
2642
|
try {
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
onLog: (evt) => {
|
|
2086
|
-
if (evt.source === "phase") {
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
if (showHeadlines) {
|
|
2090
|
-
buffer.push(evt.chunk);
|
|
2091
|
-
} else {
|
|
2092
|
-
process.stderr.write(evt.chunk);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
});
|
|
2096
|
-
if (json) {
|
|
2097
|
-
emitJson({
|
|
2098
|
-
schema_version: 1,
|
|
2099
|
-
snap_dir: res.snapDir,
|
|
2100
|
-
elapsed_ms: res.elapsedMs,
|
|
2101
|
-
dry_run: false
|
|
2102
|
-
});
|
|
2103
|
-
} else {
|
|
2104
|
-
process.stdout.write(`snapshot: ${res.snapDir} (${res.elapsedMs}ms)
|
|
2105
|
-
`);
|
|
2643
|
+
const fork = await startForkVm(vm, opts);
|
|
2644
|
+
reportForkStarted(fork, opts);
|
|
2645
|
+
if (opts.parsed.detach) {
|
|
2646
|
+
return detachFork(fork, opts);
|
|
2106
2647
|
}
|
|
2107
|
-
return
|
|
2648
|
+
return runForkAttachedSession(fork, opts.quiet);
|
|
2108
2649
|
} catch (err) {
|
|
2109
|
-
if (showHeadlines) {
|
|
2110
|
-
failQuiet(`snapshot ${snapHeadlineName} failed: ${describeError(err)}`, {
|
|
2111
|
-
buffer
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
2650
|
handleError(err);
|
|
2115
2651
|
} finally {
|
|
2116
2652
|
await vm.detach();
|
|
2117
2653
|
}
|
|
2118
2654
|
}
|
|
2119
|
-
async function
|
|
2120
|
-
const { json, rest
|
|
2121
|
-
|
|
2655
|
+
async function prepareForkCommand(args) {
|
|
2656
|
+
const { json, rest } = consumeJsonFlag(args);
|
|
2657
|
+
const parsed = parseForkCommandArgs(rest);
|
|
2658
|
+
const target = parseTargetFlags(parsed.rest, "fork");
|
|
2659
|
+
validateForkCommand(json, parsed);
|
|
2660
|
+
const paths = await resolveCliBaseAssets();
|
|
2661
|
+
const resolvedOutDir = resolveForkOutDir(parsed.outDir);
|
|
2662
|
+
return {
|
|
2663
|
+
json,
|
|
2664
|
+
target,
|
|
2665
|
+
parsed,
|
|
2666
|
+
paths,
|
|
2667
|
+
resolvedOutDir,
|
|
2668
|
+
quiet: createForkQuietState(json, parsed, target)
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function parseForkCommandArgs(args) {
|
|
2122
2672
|
try {
|
|
2123
|
-
|
|
2673
|
+
return parseForkArgs(args);
|
|
2124
2674
|
} catch (err) {
|
|
2125
2675
|
handleError(err);
|
|
2126
2676
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
tcpKeep,
|
|
2131
|
-
detach,
|
|
2132
|
-
lazy,
|
|
2133
|
-
portForward,
|
|
2134
|
-
mount,
|
|
2135
|
-
liveMounts,
|
|
2136
|
-
env,
|
|
2137
|
-
guestCwd,
|
|
2138
|
-
memory,
|
|
2139
|
-
rest
|
|
2140
|
-
} = parsed;
|
|
2141
|
-
const target = parseTargetFlags(rest, "fork");
|
|
2142
|
-
if (json && !detach) {
|
|
2677
|
+
}
|
|
2678
|
+
function validateForkCommand(json, parsed) {
|
|
2679
|
+
if (json && !parsed.detach) {
|
|
2143
2680
|
die("fork --json is only meaningful with --detach (attached forks take over stdio).");
|
|
2144
2681
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
process.stderr.write(`machinen: fetching base assets for ${RELEASE_TAG} (first run)
|
|
2150
|
-
`);
|
|
2151
|
-
await ensureBaseAssets(RELEASE_TAG);
|
|
2682
|
+
}
|
|
2683
|
+
function resolveForkOutDir(outDir) {
|
|
2684
|
+
if (outDir) {
|
|
2685
|
+
return resolve(outDir);
|
|
2152
2686
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
const
|
|
2158
|
-
const vm = await attach(target).catch(handleError);
|
|
2159
|
-
const sourceLabel = "name" in target ? target.name : `pid ${target.pid}`;
|
|
2160
|
-
const forkHeadlineName = newName ?? sourceLabel;
|
|
2161
|
-
const showHeadlines = isQuiet() && !(detach && json);
|
|
2162
|
-
const forkT0 = Date.now();
|
|
2687
|
+
return mkdtempSync(join2(tmpdir(), "machinen-fork-"));
|
|
2688
|
+
}
|
|
2689
|
+
function createForkQuietState(json, parsed, target) {
|
|
2690
|
+
const sourceLabel = describeForkSource(target);
|
|
2691
|
+
const headlineName = parsed.newName ?? sourceLabel;
|
|
2163
2692
|
const buffer = new RingBuffer();
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
printHeadline(`forking ${sourceLabel} \u2192 ${forkHeadlineName}\u2026`);
|
|
2167
|
-
if (!detach) {
|
|
2168
|
-
filter = new NoiseFilter({
|
|
2169
|
-
buffer,
|
|
2170
|
-
out: process.stderr,
|
|
2171
|
-
onReady: () => {
|
|
2172
|
-
printHeadline(`fork ready in ${formatElapsed(Date.now() - forkT0)}`);
|
|
2173
|
-
}
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2693
|
+
if (!shouldShowForkHeadlines(json, parsed)) {
|
|
2694
|
+
return forkOperatorQuietState(headlineName, buffer);
|
|
2176
2695
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2696
|
+
return createVisibleForkQuietState(parsed, sourceLabel, headlineName, buffer);
|
|
2697
|
+
}
|
|
2698
|
+
function shouldShowForkHeadlines(json, parsed) {
|
|
2699
|
+
if (!isQuiet()) {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
if (parsed.detach && json) {
|
|
2703
|
+
return false;
|
|
2704
|
+
}
|
|
2705
|
+
return true;
|
|
2706
|
+
}
|
|
2707
|
+
function createVisibleForkQuietState(parsed, sourceLabel, headlineName, buffer) {
|
|
2708
|
+
printHeadline(`forking ${sourceLabel} \u2192 ${headlineName}\u2026`);
|
|
2709
|
+
if (parsed.detach) {
|
|
2710
|
+
return bootBufferOnlyQuietState(headlineName, true, buffer);
|
|
2711
|
+
}
|
|
2712
|
+
return forkFilteredQuietState(headlineName, true, buffer, Date.now());
|
|
2713
|
+
}
|
|
2714
|
+
function describeForkSource(target) {
|
|
2715
|
+
return "name" in target ? target.name : `pid ${target.pid}`;
|
|
2716
|
+
}
|
|
2717
|
+
function forkOperatorQuietState(headlineName, buffer) {
|
|
2718
|
+
return {
|
|
2719
|
+
headlineName,
|
|
2720
|
+
showHeadlines: false,
|
|
2721
|
+
buffer,
|
|
2722
|
+
filter: null,
|
|
2723
|
+
filterOut: null,
|
|
2724
|
+
onLog: operatorForkOnLog
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
function operatorForkOnLog(evt) {
|
|
2728
|
+
if (evt.source !== "phase") {
|
|
2729
|
+
process.stderr.write(evt.chunk);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function forkFilteredQuietState(headlineName, showHeadlines, buffer, forkT0) {
|
|
2733
|
+
const filter = new NoiseFilter({
|
|
2734
|
+
buffer,
|
|
2735
|
+
out: process.stderr,
|
|
2736
|
+
onReady: () => {
|
|
2737
|
+
printHeadline(`fork ready in ${formatElapsed(Date.now() - forkT0)}`);
|
|
2188
2738
|
}
|
|
2739
|
+
});
|
|
2740
|
+
return {
|
|
2741
|
+
headlineName,
|
|
2742
|
+
showHeadlines,
|
|
2743
|
+
buffer,
|
|
2744
|
+
filter,
|
|
2745
|
+
filterOut: null,
|
|
2746
|
+
onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
|
|
2189
2747
|
};
|
|
2748
|
+
}
|
|
2749
|
+
async function startForkVm(vm, opts) {
|
|
2190
2750
|
try {
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2751
|
+
return await vm.fork({
|
|
2752
|
+
name: opts.parsed.newName,
|
|
2753
|
+
outDir: opts.resolvedOutDir,
|
|
2754
|
+
image: opts.paths.defaultImagePath,
|
|
2755
|
+
kernel: opts.paths.kernelPath,
|
|
2756
|
+
dtb: opts.paths.dtbPath,
|
|
2757
|
+
tcpKeep: opts.parsed.tcpKeep,
|
|
2758
|
+
lazy: opts.parsed.lazy,
|
|
2759
|
+
portForward: optionalList(opts.parsed.portForward),
|
|
2760
|
+
mount: opts.parsed.mount,
|
|
2761
|
+
liveMounts: opts.parsed.liveMounts,
|
|
2762
|
+
env: opts.parsed.env,
|
|
2763
|
+
guestCwd: opts.parsed.guestCwd,
|
|
2764
|
+
memory: opts.parsed.memory,
|
|
2765
|
+
onLog: opts.quiet.onLog
|
|
2766
|
+
});
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
handleForkFailure(err, opts.quiet);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
function handleForkFailure(err, quiet) {
|
|
2772
|
+
quiet.filter?.flush();
|
|
2773
|
+
if (quiet.showHeadlines) {
|
|
2774
|
+
failQuiet(`fork ${quiet.headlineName} failed: ${describeError(err)}`, {
|
|
2775
|
+
buffer: quiet.buffer
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
handleError(err);
|
|
2779
|
+
}
|
|
2780
|
+
function reportForkStarted(fork, opts) {
|
|
2781
|
+
if (!shouldPrintForkStarted(opts)) {
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
process.stderr.write(`forked: ${fork.name ?? "<anonymous>"} (pid ${fork.pid})
|
|
2220
2785
|
`);
|
|
2221
|
-
|
|
2222
|
-
|
|
2786
|
+
printForkBundleHint(opts);
|
|
2787
|
+
}
|
|
2788
|
+
function shouldPrintForkStarted(opts) {
|
|
2789
|
+
if (opts.quiet.showHeadlines) {
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
return !opts.json;
|
|
2793
|
+
}
|
|
2794
|
+
function printForkBundleHint(opts) {
|
|
2795
|
+
if (!opts.parsed.outDir) {
|
|
2796
|
+
process.stderr.write(`bundle: ${opts.resolvedOutDir} (rm -rf when the fork exits)
|
|
2223
2797
|
`);
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
try {
|
|
2248
|
-
fork.stdin.write("\r");
|
|
2249
|
-
} catch {
|
|
2250
|
-
}
|
|
2251
|
-
}, 1500);
|
|
2252
|
-
promptNudge.unref();
|
|
2253
|
-
let forwardedSignal = null;
|
|
2254
|
-
const onSigint = () => {
|
|
2255
|
-
forwardedSignal = "SIGINT";
|
|
2256
|
-
void fork.kill();
|
|
2257
|
-
};
|
|
2258
|
-
const onSigterm = () => {
|
|
2259
|
-
forwardedSignal = "SIGTERM";
|
|
2260
|
-
void fork.kill();
|
|
2261
|
-
};
|
|
2262
|
-
process.on("SIGINT", onSigint);
|
|
2263
|
-
process.on("SIGTERM", onSigterm);
|
|
2264
|
-
pipeStdinToVm(fork.stdin, () => {
|
|
2265
|
-
process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
|
|
2266
|
-
forwardedSignal = "SIGTERM";
|
|
2267
|
-
void fork.kill();
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
async function detachFork(fork, opts) {
|
|
2801
|
+
await fork.detach();
|
|
2802
|
+
if (opts.json) {
|
|
2803
|
+
emitJson({
|
|
2804
|
+
schema_version: 1,
|
|
2805
|
+
pid: fork.pid,
|
|
2806
|
+
name: fork.name ?? null,
|
|
2807
|
+
source: describeForkSource(opts.target),
|
|
2808
|
+
bundle_dir: opts.resolvedOutDir,
|
|
2809
|
+
ephemeral: !opts.parsed.outDir
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
return 0;
|
|
2813
|
+
}
|
|
2814
|
+
async function runForkAttachedSession(fork, quiet) {
|
|
2815
|
+
const cancelPromptNudge = scheduleForkPromptNudge(fork);
|
|
2816
|
+
try {
|
|
2817
|
+
return await runAttachedVmSession(fork, {
|
|
2818
|
+
filter: quiet.filter,
|
|
2819
|
+
buffer: quiet.buffer,
|
|
2820
|
+
preReadyExitSummary: (code) => `fork ${quiet.headlineName} exited ${code} before reaching ready`
|
|
2268
2821
|
});
|
|
2269
|
-
try {
|
|
2270
|
-
const { code } = await fork.wait();
|
|
2271
|
-
filter?.flush();
|
|
2272
|
-
if (forwardedSignal === "SIGINT") {
|
|
2273
|
-
return 130;
|
|
2274
|
-
}
|
|
2275
|
-
if (forwardedSignal === "SIGTERM") {
|
|
2276
|
-
return 143;
|
|
2277
|
-
}
|
|
2278
|
-
if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
|
|
2279
|
-
printDiagnostics(`fork ${forkHeadlineName} exited ${code} before reaching ready`, {
|
|
2280
|
-
buffer
|
|
2281
|
-
});
|
|
2282
|
-
}
|
|
2283
|
-
return code ?? 0;
|
|
2284
|
-
} finally {
|
|
2285
|
-
process.off("SIGINT", onSigint);
|
|
2286
|
-
process.off("SIGTERM", onSigterm);
|
|
2287
|
-
cancelHintRepeat();
|
|
2288
|
-
restoreStdin();
|
|
2289
|
-
}
|
|
2290
|
-
} catch (err) {
|
|
2291
|
-
handleError(err);
|
|
2292
2822
|
} finally {
|
|
2293
|
-
|
|
2823
|
+
cancelPromptNudge();
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
function scheduleForkPromptNudge(fork) {
|
|
2827
|
+
const promptNudge = setTimeout(() => tryWriteForkPromptNudge(fork), 1500);
|
|
2828
|
+
promptNudge.unref();
|
|
2829
|
+
return () => clearTimeout(promptNudge);
|
|
2830
|
+
}
|
|
2831
|
+
function tryWriteForkPromptNudge(fork) {
|
|
2832
|
+
try {
|
|
2833
|
+
fork.stdin.write("\r");
|
|
2834
|
+
} catch {
|
|
2294
2835
|
}
|
|
2295
2836
|
}
|
|
2296
2837
|
async function cmdAttach(args) {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
const
|
|
2838
|
+
const opts = parseAttachOptions(args);
|
|
2839
|
+
printAttachTailIfRequested(opts);
|
|
2840
|
+
const vm = await attach(opts.target).catch(handleError);
|
|
2841
|
+
return runAttachedPty(vm, opts.shell);
|
|
2842
|
+
}
|
|
2843
|
+
function parseAttachOptions(args) {
|
|
2844
|
+
const state = {
|
|
2845
|
+
shell: "/bin/bash -i",
|
|
2846
|
+
tail: void 0,
|
|
2847
|
+
rest: []
|
|
2848
|
+
};
|
|
2300
2849
|
for (let i = 0; i < args.length; i++) {
|
|
2301
|
-
|
|
2302
|
-
if (a === "--shell" || a.startsWith("--shell=")) {
|
|
2303
|
-
const v = a === "--shell" ? args[++i] : a.slice("--shell=".length);
|
|
2304
|
-
if (!v) {
|
|
2305
|
-
die("--shell requires a value");
|
|
2306
|
-
}
|
|
2307
|
-
shell = v;
|
|
2308
|
-
} else if (a === "--tail" || a.startsWith("--tail=")) {
|
|
2309
|
-
let v;
|
|
2310
|
-
if (a === "--tail") {
|
|
2311
|
-
const peek = args[i + 1];
|
|
2312
|
-
if (peek && /^[0-9]+$/.test(peek)) {
|
|
2313
|
-
v = peek;
|
|
2314
|
-
i++;
|
|
2315
|
-
}
|
|
2316
|
-
} else {
|
|
2317
|
-
v = a.slice("--tail=".length);
|
|
2318
|
-
}
|
|
2319
|
-
if (v === void 0) {
|
|
2320
|
-
tail = "all";
|
|
2321
|
-
} else {
|
|
2322
|
-
const n = Number(v);
|
|
2323
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
2324
|
-
die(`--tail: expected a non-negative integer, got '${v}'`);
|
|
2325
|
-
}
|
|
2326
|
-
tail = n;
|
|
2327
|
-
}
|
|
2328
|
-
} else {
|
|
2329
|
-
filtered.push(a);
|
|
2330
|
-
}
|
|
2850
|
+
i = consumeAttachArg(args, i, state);
|
|
2331
2851
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
die(
|
|
2340
|
-
`machinen attach --tail: VM was not booted with --detached, no snapshot exists. Use 'machinen attach' (no --tail) for live console access.`
|
|
2341
|
-
);
|
|
2342
|
-
}
|
|
2343
|
-
printBootLogTail(entry.bootLogPath, tail);
|
|
2852
|
+
return { shell: state.shell, tail: state.tail, target: parseTargetFlags(state.rest, "attach") };
|
|
2853
|
+
}
|
|
2854
|
+
function consumeAttachArg(args, index, state) {
|
|
2855
|
+
const arg = args[index];
|
|
2856
|
+
const handler = attachArgHandler(arg);
|
|
2857
|
+
if (handler) {
|
|
2858
|
+
return handler(args, index, arg, state);
|
|
2344
2859
|
}
|
|
2345
|
-
|
|
2860
|
+
state.rest.push(arg);
|
|
2861
|
+
return index;
|
|
2862
|
+
}
|
|
2863
|
+
var ATTACH_ARG_HANDLERS = [
|
|
2864
|
+
[(arg) => arg === "--shell" || arg.startsWith("--shell="), consumeAttachShell],
|
|
2865
|
+
[(arg) => arg === "--tail" || arg.startsWith("--tail="), consumeAttachTail]
|
|
2866
|
+
];
|
|
2867
|
+
function attachArgHandler(arg) {
|
|
2868
|
+
return ATTACH_ARG_HANDLERS.find(([matches]) => matches(arg))?.[1];
|
|
2869
|
+
}
|
|
2870
|
+
function consumeAttachShell(args, index, arg, state) {
|
|
2871
|
+
const value = arg === "--shell" ? args[index + 1] : arg.slice("--shell=".length);
|
|
2872
|
+
if (!value) {
|
|
2873
|
+
die("--shell requires a value");
|
|
2874
|
+
}
|
|
2875
|
+
state.shell = value;
|
|
2876
|
+
return arg === "--shell" ? index + 1 : index;
|
|
2877
|
+
}
|
|
2878
|
+
function consumeAttachTail(args, index, arg, state) {
|
|
2879
|
+
const { value, nextIndex } = attachTailValue(args, index, arg);
|
|
2880
|
+
state.tail = parseAttachTail(value);
|
|
2881
|
+
return nextIndex;
|
|
2882
|
+
}
|
|
2883
|
+
function attachTailValue(args, index, arg) {
|
|
2884
|
+
if (arg !== "--tail") {
|
|
2885
|
+
return { value: arg.slice("--tail=".length), nextIndex: index };
|
|
2886
|
+
}
|
|
2887
|
+
const peek = args[index + 1];
|
|
2888
|
+
if (peek && /^[0-9]+$/.test(peek)) {
|
|
2889
|
+
return { value: peek, nextIndex: index + 1 };
|
|
2890
|
+
}
|
|
2891
|
+
return { value: void 0, nextIndex: index };
|
|
2892
|
+
}
|
|
2893
|
+
function parseAttachTail(value) {
|
|
2894
|
+
if (value === void 0) {
|
|
2895
|
+
return "all";
|
|
2896
|
+
}
|
|
2897
|
+
const n = Number(value);
|
|
2898
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
2899
|
+
die(`--tail: expected a non-negative integer, got '${value}'`);
|
|
2900
|
+
}
|
|
2901
|
+
return n;
|
|
2902
|
+
}
|
|
2903
|
+
function printAttachTailIfRequested(opts) {
|
|
2904
|
+
if (opts.tail === void 0) {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const entry = lookupAttachTailEntry(opts.target);
|
|
2908
|
+
printBootLogTail(entry.bootLogPath, opts.tail);
|
|
2909
|
+
}
|
|
2910
|
+
function lookupAttachTailEntry(target) {
|
|
2911
|
+
const entry = lookupEntry(target);
|
|
2912
|
+
if (!entry) {
|
|
2913
|
+
die(`machinen attach: no running VM matched ${describeTarget(target)}`);
|
|
2914
|
+
}
|
|
2915
|
+
if (!entry.bootLogPath) {
|
|
2916
|
+
die(
|
|
2917
|
+
`machinen attach --tail: VM was not booted with --detached, no snapshot exists. Use 'machinen attach' (no --tail) for live console access.`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
return entry;
|
|
2921
|
+
}
|
|
2922
|
+
async function runAttachedPty(vm, shell) {
|
|
2346
2923
|
if (!process.stdin.isTTY) {
|
|
2347
2924
|
await vm.detach();
|
|
2348
2925
|
die("machinen attach: stdin is not a TTY (pipe scripts via `machinen repl` instead)");
|
|
@@ -2371,6 +2948,15 @@ function printBootLogTail(path, tail) {
|
|
|
2371
2948
|
async function cmdRepl(args) {
|
|
2372
2949
|
const target = parseTargetFlags(args, "repl");
|
|
2373
2950
|
const vm = await attach(target).catch(handleError);
|
|
2951
|
+
printReplIntro(vm);
|
|
2952
|
+
try {
|
|
2953
|
+
await runReplLoop(vm);
|
|
2954
|
+
return 0;
|
|
2955
|
+
} finally {
|
|
2956
|
+
await vm.detach();
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
function printReplIntro(vm) {
|
|
2374
2960
|
process.stderr.write(`repl: ${vm.name ?? `pid ${vm.pid}`}
|
|
2375
2961
|
`);
|
|
2376
2962
|
process.stderr.write(
|
|
@@ -2380,22 +2966,22 @@ for an interactive shell with job control + TUI support, use:
|
|
|
2380
2966
|
Ctrl-D to exit.
|
|
2381
2967
|
`
|
|
2382
2968
|
);
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
}
|
|
2395
|
-
return 0;
|
|
2396
|
-
} finally {
|
|
2397
|
-
await vm.detach();
|
|
2969
|
+
}
|
|
2970
|
+
async function runReplLoop(vm) {
|
|
2971
|
+
const { createInterface } = await import("readline");
|
|
2972
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
2973
|
+
for await (const line of rl) {
|
|
2974
|
+
await runReplLine(vm, line);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function runReplLine(vm, line) {
|
|
2978
|
+
if (line.length === 0) {
|
|
2979
|
+
return;
|
|
2398
2980
|
}
|
|
2981
|
+
await vm.execRaw(line, {
|
|
2982
|
+
onStdout: (chunk) => process.stdout.write(chunk),
|
|
2983
|
+
onStderr: (chunk) => process.stderr.write(chunk)
|
|
2984
|
+
});
|
|
2399
2985
|
}
|
|
2400
2986
|
async function cmdAgentContext(args) {
|
|
2401
2987
|
for (const a of args) {
|
|
@@ -2407,86 +2993,103 @@ async function cmdAgentContext(args) {
|
|
|
2407
2993
|
return 0;
|
|
2408
2994
|
}
|
|
2409
2995
|
async function cmdFeedback(args) {
|
|
2410
|
-
const
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
for (const a of afterJson) {
|
|
2414
|
-
if (a === "--list") {
|
|
2415
|
-
listMode = true;
|
|
2416
|
-
} else if (a.startsWith("--")) {
|
|
2417
|
-
die(`unknown argument: ${a}`);
|
|
2418
|
-
} else {
|
|
2419
|
-
positional.push(a);
|
|
2420
|
-
}
|
|
2996
|
+
const opts = parseFeedbackOptions(args);
|
|
2997
|
+
if (opts.listMode) {
|
|
2998
|
+
return listFeedback(opts);
|
|
2421
2999
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
}
|
|
3000
|
+
return recordFeedback(opts);
|
|
3001
|
+
}
|
|
3002
|
+
function parseFeedbackOptions(args) {
|
|
3003
|
+
const { json, rest } = consumeJsonFlag(args);
|
|
3004
|
+
const opts = { json, listMode: false, positional: [] };
|
|
3005
|
+
for (const arg of rest) {
|
|
3006
|
+
consumeFeedbackArg(opts, arg);
|
|
3007
|
+
}
|
|
3008
|
+
return opts;
|
|
3009
|
+
}
|
|
3010
|
+
function consumeFeedbackArg(opts, arg) {
|
|
3011
|
+
if (arg === "--list") {
|
|
3012
|
+
opts.listMode = true;
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
if (arg.startsWith("--")) {
|
|
3016
|
+
die(`unknown argument: ${arg}`);
|
|
3017
|
+
}
|
|
3018
|
+
opts.positional.push(arg);
|
|
3019
|
+
}
|
|
3020
|
+
function listFeedback(opts) {
|
|
3021
|
+
if (opts.positional.length > 0) {
|
|
3022
|
+
die("machinen feedback --list takes no positional arguments");
|
|
3023
|
+
}
|
|
3024
|
+
const entries = readFeedback();
|
|
3025
|
+
if (opts.json) {
|
|
3026
|
+
emitJson({ schema_version: 1, entries });
|
|
2439
3027
|
return 0;
|
|
2440
3028
|
}
|
|
2441
|
-
|
|
3029
|
+
printFeedbackEntries(entries);
|
|
3030
|
+
return 0;
|
|
3031
|
+
}
|
|
3032
|
+
function printFeedbackEntries(entries) {
|
|
3033
|
+
if (entries.length === 0) {
|
|
3034
|
+
process.stdout.write("(no feedback recorded)\n");
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
for (const entry of entries) {
|
|
3038
|
+
process.stdout.write(`${entry.timestamp} ${entry.text}
|
|
3039
|
+
`);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
async function recordFeedback(opts) {
|
|
3043
|
+
if (opts.positional.length === 0) {
|
|
2442
3044
|
die('usage: machinen feedback "<text>" | machinen feedback --list');
|
|
2443
3045
|
}
|
|
2444
|
-
const text = positional.join(" ");
|
|
2445
3046
|
const path = feedbackPath();
|
|
2446
|
-
const entry =
|
|
3047
|
+
const entry = newFeedbackEntry(opts.positional.join(" "));
|
|
3048
|
+
appendFeedback(entry, path);
|
|
3049
|
+
const upstream = await postUpstream(entry);
|
|
3050
|
+
reportFeedbackRecorded(opts, path, upstream);
|
|
3051
|
+
return 0;
|
|
3052
|
+
}
|
|
3053
|
+
function newFeedbackEntry(text) {
|
|
3054
|
+
return {
|
|
2447
3055
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2448
3056
|
cli_version: VERSION,
|
|
2449
3057
|
text
|
|
2450
3058
|
};
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
if (json) {
|
|
2454
|
-
emitJson({
|
|
2455
|
-
|
|
2456
|
-
recorded: true,
|
|
2457
|
-
path,
|
|
2458
|
-
upstream_status: upstream.status
|
|
2459
|
-
});
|
|
2460
|
-
return 0;
|
|
3059
|
+
}
|
|
3060
|
+
function reportFeedbackRecorded(opts, path, upstream) {
|
|
3061
|
+
if (opts.json) {
|
|
3062
|
+
emitJson({ schema_version: 1, recorded: true, path, upstream_status: upstream.status });
|
|
3063
|
+
return;
|
|
2461
3064
|
}
|
|
3065
|
+
process.stdout.write(feedbackRecordedMessage(upstream));
|
|
3066
|
+
}
|
|
3067
|
+
function feedbackRecordedMessage(upstream) {
|
|
2462
3068
|
if (upstream.attempted && upstream.status !== null) {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
`
|
|
2466
|
-
);
|
|
2467
|
-
} else if (upstream.attempted) {
|
|
2468
|
-
process.stdout.write(`feedback recorded locally; upstream POST failed: ${upstream.error}
|
|
2469
|
-
`);
|
|
2470
|
-
} else {
|
|
2471
|
-
process.stdout.write("feedback recorded locally (1 entry)\n");
|
|
3069
|
+
return `feedback recorded locally and sent upstream (status: ${upstream.status})
|
|
3070
|
+
`;
|
|
2472
3071
|
}
|
|
2473
|
-
|
|
3072
|
+
if (upstream.attempted) {
|
|
3073
|
+
return `feedback recorded locally; upstream POST failed: ${upstream.error}
|
|
3074
|
+
`;
|
|
3075
|
+
}
|
|
3076
|
+
return "feedback recorded locally (1 entry)\n";
|
|
2474
3077
|
}
|
|
2475
3078
|
async function cmdCompletion(args) {
|
|
2476
3079
|
const shell = args[0] ?? "bash";
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
}
|
|
2481
|
-
if (shell === "zsh") {
|
|
2482
|
-
process.stdout.write(ZSH_COMPLETION);
|
|
2483
|
-
return 0;
|
|
2484
|
-
}
|
|
2485
|
-
if (shell === "fish") {
|
|
2486
|
-
process.stdout.write(FISH_COMPLETION);
|
|
2487
|
-
return 0;
|
|
3080
|
+
const completion = completionForShell(shell);
|
|
3081
|
+
if (completion === void 0) {
|
|
3082
|
+
die(`unsupported shell: ${shell} (expected bash | zsh | fish)`);
|
|
2488
3083
|
}
|
|
2489
|
-
|
|
3084
|
+
process.stdout.write(completion);
|
|
3085
|
+
return 0;
|
|
3086
|
+
}
|
|
3087
|
+
function completionForShell(shell) {
|
|
3088
|
+
return (/* @__PURE__ */ new Map([
|
|
3089
|
+
["bash", BASH_COMPLETION],
|
|
3090
|
+
["zsh", ZSH_COMPLETION],
|
|
3091
|
+
["fish", FISH_COMPLETION]
|
|
3092
|
+
])).get(shell);
|
|
2490
3093
|
}
|
|
2491
3094
|
function resolveTarget(args, cmd) {
|
|
2492
3095
|
try {
|
|
@@ -2630,6 +3233,8 @@ Usage:
|
|
|
2630
3233
|
--env KEY=VALUE Set an env var inside the guest.
|
|
2631
3234
|
--cwd <abs-path> Start the guest cmd in this directory
|
|
2632
3235
|
(must be absolute).
|
|
3236
|
+
--nested Expose arm64 EL2 / /dev/kvm to the guest
|
|
3237
|
+
when the host supports it.
|
|
2633
3238
|
-p <hostPort>:<guestPort> Forward host:hostPort \u2192 guest:guestPort.
|
|
2634
3239
|
|
|
2635
3240
|
machinen restore <snap-dir> [--image <tar.gz>] [--name <name>] [-p ...]
|
|
@@ -2664,12 +3269,11 @@ Usage:
|
|
|
2664
3269
|
Example:
|
|
2665
3270
|
machinen exec <name|pid> --tty -- bash -i
|
|
2666
3271
|
machinen snapshot <name|pid> <out-dir> [--keep-alive]
|
|
2667
|
-
|
|
2668
|
-
Default
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
connection state).
|
|
3272
|
+
Checkpoint a running VM into <d>.
|
|
3273
|
+
Default vmstate snapshots are incremental
|
|
3274
|
+
and non-destructive. CRIU snapshots stay
|
|
3275
|
+
non-incremental; --keep-alive leaves them
|
|
3276
|
+
running and closes inherited TCP sockets.
|
|
2673
3277
|
machinen fork <name|pid> [--new-name <n>] [--out-dir <d>] [--tcp-keep] [--detach]
|
|
2674
3278
|
[-p ...] [--mount ...] [--mount-live ...] [--env KEY=VALUE]...
|
|
2675
3279
|
[--cwd <abs>] [--memory <mib>]
|
|
@@ -2736,61 +3340,94 @@ Environment:
|
|
|
2736
3340
|
MACHINEN_VMM Override the VMM binary path (dev)
|
|
2737
3341
|
MACHINEN_ASSETS_DIR Use base assets from this directory
|
|
2738
3342
|
instead of the cache / GH Releases
|
|
3343
|
+
MACHINEN_GUEST_ARCH Guest asset arch: arm64 or amd64
|
|
3344
|
+
MACHINEN_SNAPSHOT_ENGINE Snapshot engine: vmstate (default),
|
|
3345
|
+
criu, or portable (experimental;
|
|
3346
|
+
unsupported workload today)
|
|
2739
3347
|
MACHINEN_REGISTRY_DIR Override registry location (default
|
|
2740
3348
|
~/.machinen/vms)
|
|
2741
3349
|
|
|
2742
3350
|
Cache:
|
|
2743
|
-
~/.machinen/<tag>/bases/debian-arm64/
|
|
3351
|
+
~/.machinen/<tag>/bases/debian-arm64/ or debian-amd64/
|
|
2744
3352
|
`
|
|
2745
3353
|
);
|
|
2746
3354
|
}
|
|
3355
|
+
var COMMAND_HANDLERS = /* @__PURE__ */ new Map([
|
|
3356
|
+
["boot", cmdBoot],
|
|
3357
|
+
["restore", cmdRestore],
|
|
3358
|
+
["install", cmdInstall],
|
|
3359
|
+
["list", cmdLs],
|
|
3360
|
+
["ls", cmdLs],
|
|
3361
|
+
["ps", cmdLs],
|
|
3362
|
+
["exec", cmdExec],
|
|
3363
|
+
["snapshot", cmdSnapshot],
|
|
3364
|
+
["fork", cmdFork],
|
|
3365
|
+
["attach", cmdAttach],
|
|
3366
|
+
["repl", cmdRepl],
|
|
3367
|
+
["completion", cmdCompletion],
|
|
3368
|
+
["gc", cmdGc],
|
|
3369
|
+
["stop", cmdStop],
|
|
3370
|
+
["feedback", cmdFeedback],
|
|
3371
|
+
["agent-context", cmdAgentContext]
|
|
3372
|
+
]);
|
|
2747
3373
|
async function main() {
|
|
2748
3374
|
const [sub, ...rest] = process.argv.slice(2);
|
|
2749
|
-
debug("dispatch sub=%s argc=%d", sub
|
|
2750
|
-
|
|
3375
|
+
debug("dispatch sub=%s argc=%d", commandLabel(sub), rest.length);
|
|
3376
|
+
const topLevelCode = maybeHandleTopLevelCommand(sub);
|
|
3377
|
+
if (topLevelCode !== void 0) {
|
|
3378
|
+
return topLevelCode;
|
|
3379
|
+
}
|
|
3380
|
+
return dispatchSubcommand(sub, rest);
|
|
3381
|
+
}
|
|
3382
|
+
function commandLabel(sub) {
|
|
3383
|
+
if (sub === void 0) {
|
|
3384
|
+
return "<empty>";
|
|
3385
|
+
}
|
|
3386
|
+
return sub;
|
|
3387
|
+
}
|
|
3388
|
+
function maybeHandleTopLevelCommand(sub) {
|
|
3389
|
+
const helpCode = maybePrintTopLevelHelp(sub);
|
|
3390
|
+
if (helpCode !== void 0) {
|
|
3391
|
+
return helpCode;
|
|
3392
|
+
}
|
|
3393
|
+
return maybePrintVersion(sub);
|
|
3394
|
+
}
|
|
3395
|
+
function dispatchSubcommand(sub, rest) {
|
|
3396
|
+
const handler = COMMAND_HANDLERS.get(sub);
|
|
3397
|
+
if (handler) {
|
|
3398
|
+
return handler(rest);
|
|
3399
|
+
}
|
|
3400
|
+
die(`unknown command: ${sub}
|
|
3401
|
+
Run 'machinen --help' for usage.`);
|
|
3402
|
+
}
|
|
3403
|
+
function maybePrintTopLevelHelp(sub) {
|
|
3404
|
+
if (!sub) {
|
|
2751
3405
|
printHelp();
|
|
2752
|
-
return
|
|
3406
|
+
return 1;
|
|
2753
3407
|
}
|
|
2754
|
-
if (sub === "
|
|
2755
|
-
|
|
2756
|
-
`);
|
|
3408
|
+
if (sub === "-h") {
|
|
3409
|
+
printHelp();
|
|
2757
3410
|
return 0;
|
|
2758
3411
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
case "restore":
|
|
2763
|
-
return cmdRestore(rest);
|
|
2764
|
-
case "install":
|
|
2765
|
-
return cmdInstall(rest);
|
|
2766
|
-
case "list":
|
|
2767
|
-
case "ls":
|
|
2768
|
-
case "ps":
|
|
2769
|
-
return cmdLs(rest);
|
|
2770
|
-
case "exec":
|
|
2771
|
-
return cmdExec(rest);
|
|
2772
|
-
case "snapshot":
|
|
2773
|
-
return cmdSnapshot(rest);
|
|
2774
|
-
case "fork":
|
|
2775
|
-
return cmdFork(rest);
|
|
2776
|
-
case "attach":
|
|
2777
|
-
return cmdAttach(rest);
|
|
2778
|
-
case "repl":
|
|
2779
|
-
return cmdRepl(rest);
|
|
2780
|
-
case "completion":
|
|
2781
|
-
return cmdCompletion(rest);
|
|
2782
|
-
case "gc":
|
|
2783
|
-
return cmdGc(rest);
|
|
2784
|
-
case "stop":
|
|
2785
|
-
return cmdStop(rest);
|
|
2786
|
-
case "feedback":
|
|
2787
|
-
return cmdFeedback(rest);
|
|
2788
|
-
case "agent-context":
|
|
2789
|
-
return cmdAgentContext(rest);
|
|
2790
|
-
default:
|
|
2791
|
-
die(`unknown command: ${sub}
|
|
2792
|
-
Run 'machinen --help' for usage.`);
|
|
3412
|
+
if (sub === "--help") {
|
|
3413
|
+
printHelp();
|
|
3414
|
+
return 0;
|
|
2793
3415
|
}
|
|
3416
|
+
return void 0;
|
|
3417
|
+
}
|
|
3418
|
+
function maybePrintVersion(sub) {
|
|
3419
|
+
if (sub === "--version") {
|
|
3420
|
+
return printVersion();
|
|
3421
|
+
}
|
|
3422
|
+
if (sub === "-v") {
|
|
3423
|
+
return printVersion();
|
|
3424
|
+
}
|
|
3425
|
+
return void 0;
|
|
3426
|
+
}
|
|
3427
|
+
function printVersion() {
|
|
3428
|
+
process.stdout.write(`${VERSION}
|
|
3429
|
+
`);
|
|
3430
|
+
return 0;
|
|
2794
3431
|
}
|
|
2795
3432
|
main().then(
|
|
2796
3433
|
(code) => process.exit(code),
|