vmsan 0.1.0-alpha.14 → 0.1.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/agent.mjs +231 -4
- package/dist/_chunks/connect.mjs +55 -11
- package/dist/_chunks/context.mjs +9 -17
- package/dist/_chunks/create.mjs +2 -1
- package/dist/_chunks/download.mjs +4 -19
- package/dist/_chunks/errors.mjs +10 -4
- package/dist/_chunks/exec.mjs +193 -0
- package/dist/_chunks/list.mjs +1 -0
- package/dist/_chunks/network.mjs +2 -1
- package/dist/_chunks/remove.mjs +1 -0
- package/dist/_chunks/shell.mjs +1 -0
- package/dist/_chunks/start.mjs +1 -0
- package/dist/_chunks/stop.mjs +1 -0
- package/dist/_chunks/summary.mjs +1 -1
- package/dist/_chunks/timeout-extender.mjs +66 -0
- package/dist/_chunks/timeout-killer.mjs +33 -0
- package/dist/_chunks/upload.mjs +4 -19
- package/dist/_chunks/validation.mjs +1 -1
- package/dist/_chunks/vm-context.mjs +34 -0
- package/dist/_chunks/vm-state.mjs +27 -2
- package/dist/bin/cli.mjs +2 -1
- package/dist/index.d.mts +151 -8
- package/dist/index.mjs +5 -3
- package/package.json +1 -1
- package/dist/_chunks/connect2.mjs +0 -72
package/dist/_chunks/agent.mjs
CHANGED
|
@@ -1,6 +1,196 @@
|
|
|
1
1
|
import { createGzip } from "node:zlib";
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
3
|
import { pack } from "tar-stream";
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight async iterable queue for pushing events and yielding them to consumers.
|
|
6
|
+
*/
|
|
7
|
+
var AsyncQueue = class {
|
|
8
|
+
_buffer = [];
|
|
9
|
+
_waiting = [];
|
|
10
|
+
_closed = false;
|
|
11
|
+
push(item) {
|
|
12
|
+
if (this._closed) return;
|
|
13
|
+
if (this._waiting.length > 0) this._waiting.shift()({
|
|
14
|
+
value: item,
|
|
15
|
+
done: false
|
|
16
|
+
});
|
|
17
|
+
else this._buffer.push(item);
|
|
18
|
+
}
|
|
19
|
+
close() {
|
|
20
|
+
this._closed = true;
|
|
21
|
+
for (const resolve of this._waiting) resolve({
|
|
22
|
+
value: void 0,
|
|
23
|
+
done: true
|
|
24
|
+
});
|
|
25
|
+
this._waiting.length = 0;
|
|
26
|
+
}
|
|
27
|
+
async *[Symbol.asyncIterator]() {
|
|
28
|
+
while (true) if (this._buffer.length > 0) yield this._buffer.shift();
|
|
29
|
+
else if (this._closed) return;
|
|
30
|
+
else {
|
|
31
|
+
const item = await new Promise((resolve) => {
|
|
32
|
+
this._waiting.push(resolve);
|
|
33
|
+
});
|
|
34
|
+
if (item.done) return;
|
|
35
|
+
yield item.value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var CommandFinished = class {
|
|
40
|
+
cmdId;
|
|
41
|
+
exitCode;
|
|
42
|
+
stdout;
|
|
43
|
+
stderr;
|
|
44
|
+
output;
|
|
45
|
+
timedOut;
|
|
46
|
+
startedAt;
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
this.cmdId = opts.cmdId;
|
|
49
|
+
this.exitCode = opts.exitCode;
|
|
50
|
+
this.stdout = opts.stdout;
|
|
51
|
+
this.stderr = opts.stderr;
|
|
52
|
+
this.output = opts.output;
|
|
53
|
+
this.timedOut = opts.timedOut;
|
|
54
|
+
this.startedAt = opts.startedAt;
|
|
55
|
+
}
|
|
56
|
+
get ok() {
|
|
57
|
+
return this.exitCode === 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const MAX_LOG_ENTRIES = 1e5;
|
|
61
|
+
var Command = class {
|
|
62
|
+
cmdId;
|
|
63
|
+
_startedAt;
|
|
64
|
+
_exitCode = null;
|
|
65
|
+
_timedOut = false;
|
|
66
|
+
_logEntries = [];
|
|
67
|
+
_logTruncated = false;
|
|
68
|
+
_eventQueue = new AsyncQueue();
|
|
69
|
+
_completion;
|
|
70
|
+
_stdoutPromise = null;
|
|
71
|
+
_stderrPromise = null;
|
|
72
|
+
_outputPromise = null;
|
|
73
|
+
_agent;
|
|
74
|
+
constructor(init) {
|
|
75
|
+
const { agent, cmdId, startedAt, stream, signal, onStdout, onStderr } = init;
|
|
76
|
+
this._agent = agent;
|
|
77
|
+
this.cmdId = cmdId;
|
|
78
|
+
this._startedAt = startedAt;
|
|
79
|
+
let resolveCompletion;
|
|
80
|
+
let rejectCompletion;
|
|
81
|
+
this._completion = new Promise((resolve, reject) => {
|
|
82
|
+
resolveCompletion = resolve;
|
|
83
|
+
rejectCompletion = reject;
|
|
84
|
+
});
|
|
85
|
+
let onAbort;
|
|
86
|
+
if (signal) {
|
|
87
|
+
onAbort = () => {
|
|
88
|
+
this.kill().catch(() => {});
|
|
89
|
+
};
|
|
90
|
+
if (signal.aborted) this.kill().catch(() => {});
|
|
91
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
92
|
+
}
|
|
93
|
+
(async () => {
|
|
94
|
+
try {
|
|
95
|
+
for await (const event of stream) {
|
|
96
|
+
this._eventQueue.push(event);
|
|
97
|
+
switch (event.type) {
|
|
98
|
+
case "stdout":
|
|
99
|
+
case "stderr":
|
|
100
|
+
if (event.data !== void 0) {
|
|
101
|
+
if (this._logEntries.length < MAX_LOG_ENTRIES) this._logEntries.push({
|
|
102
|
+
stream: event.type,
|
|
103
|
+
data: event.data
|
|
104
|
+
});
|
|
105
|
+
else this._logTruncated = true;
|
|
106
|
+
(event.type === "stdout" ? onStdout : onStderr)?.(event.data);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "exit":
|
|
110
|
+
this._exitCode = event.exitCode ?? 1;
|
|
111
|
+
break;
|
|
112
|
+
case "timeout":
|
|
113
|
+
this._timedOut = true;
|
|
114
|
+
this._exitCode = 124;
|
|
115
|
+
break;
|
|
116
|
+
case "error":
|
|
117
|
+
this._exitCode = 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this._eventQueue.close();
|
|
122
|
+
resolveCompletion(new CommandFinished({
|
|
123
|
+
cmdId,
|
|
124
|
+
exitCode: this._exitCode ?? 1,
|
|
125
|
+
stdout: this._logEntries.filter((e) => e.stream === "stdout").map((e) => e.data).join("\n"),
|
|
126
|
+
stderr: this._logEntries.filter((e) => e.stream === "stderr").map((e) => e.data).join("\n"),
|
|
127
|
+
output: this._logEntries.map((e) => e.data).join("\n"),
|
|
128
|
+
timedOut: this._timedOut,
|
|
129
|
+
startedAt
|
|
130
|
+
}));
|
|
131
|
+
this._logEntries.length = 0;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this._eventQueue.close();
|
|
134
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
135
|
+
rejectCompletion(error);
|
|
136
|
+
} finally {
|
|
137
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
get startedAt() {
|
|
142
|
+
return this._startedAt;
|
|
143
|
+
}
|
|
144
|
+
get exitCode() {
|
|
145
|
+
return this._exitCode;
|
|
146
|
+
}
|
|
147
|
+
async *logs(opts) {
|
|
148
|
+
const signal = opts?.signal;
|
|
149
|
+
if (signal?.aborted) return;
|
|
150
|
+
for await (const event of this._eventQueue) {
|
|
151
|
+
if (signal?.aborted) return;
|
|
152
|
+
if (event.type === "stdout" || event.type === "stderr") yield {
|
|
153
|
+
stream: event.type,
|
|
154
|
+
data: event.data ?? ""
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
stdout(opts) {
|
|
159
|
+
if (!this._stdoutPromise) this._stdoutPromise = this._completion.then((r) => r.stdout);
|
|
160
|
+
return this._withSignal(this._stdoutPromise, opts?.signal);
|
|
161
|
+
}
|
|
162
|
+
stderr(opts) {
|
|
163
|
+
if (!this._stderrPromise) this._stderrPromise = this._completion.then((r) => r.stderr);
|
|
164
|
+
return this._withSignal(this._stderrPromise, opts?.signal);
|
|
165
|
+
}
|
|
166
|
+
output(stream, opts) {
|
|
167
|
+
if (stream === "stdout") return this.stdout(opts);
|
|
168
|
+
if (stream === "stderr") return this.stderr(opts);
|
|
169
|
+
if (!this._outputPromise) this._outputPromise = this._completion.then((r) => r.output);
|
|
170
|
+
return this._withSignal(this._outputPromise, opts?.signal);
|
|
171
|
+
}
|
|
172
|
+
wait(opts) {
|
|
173
|
+
return this._withSignal(this._completion, opts?.signal);
|
|
174
|
+
}
|
|
175
|
+
async kill(signal, opts) {
|
|
176
|
+
await this._agent.killCommand(this.cmdId, signal, opts?.abortSignal);
|
|
177
|
+
}
|
|
178
|
+
_withSignal(promise, signal) {
|
|
179
|
+
if (!signal) return promise;
|
|
180
|
+
if (signal.aborted) return Promise.reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
183
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
promise.then((value) => {
|
|
185
|
+
signal.removeEventListener("abort", onAbort);
|
|
186
|
+
resolve(value);
|
|
187
|
+
}, (err) => {
|
|
188
|
+
signal.removeEventListener("abort", onAbort);
|
|
189
|
+
reject(err);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
4
194
|
var AgentClient = class {
|
|
5
195
|
constructor(baseUrl, token) {
|
|
6
196
|
this.baseUrl = baseUrl;
|
|
@@ -11,14 +201,15 @@ var AgentClient = class {
|
|
|
11
201
|
if (!res.ok) throw new Error(`Agent health check failed: ${res.status}`);
|
|
12
202
|
return res.json();
|
|
13
203
|
}
|
|
14
|
-
async *run(params) {
|
|
204
|
+
async *run(params, signal) {
|
|
15
205
|
const res = await fetch(`${this.baseUrl}/exec`, {
|
|
16
206
|
method: "POST",
|
|
17
207
|
headers: {
|
|
18
208
|
"Content-Type": "application/json",
|
|
19
209
|
Authorization: `Bearer ${this.token}`
|
|
20
210
|
},
|
|
21
|
-
body: JSON.stringify(params)
|
|
211
|
+
body: JSON.stringify(params),
|
|
212
|
+
signal
|
|
22
213
|
});
|
|
23
214
|
if (!res.ok) {
|
|
24
215
|
const text = await res.text();
|
|
@@ -41,7 +232,7 @@ var AgentClient = class {
|
|
|
41
232
|
}
|
|
42
233
|
if (buffer.trim()) yield JSON.parse(buffer.trim());
|
|
43
234
|
}
|
|
44
|
-
async killCommand(cmdId, signal) {
|
|
235
|
+
async killCommand(cmdId, signal, abortSignal) {
|
|
45
236
|
const url = `${this.baseUrl}/exec/${cmdId}/kill`;
|
|
46
237
|
const res = await fetch(url, {
|
|
47
238
|
method: "POST",
|
|
@@ -49,7 +240,8 @@ var AgentClient = class {
|
|
|
49
240
|
"Content-Type": "application/json",
|
|
50
241
|
Authorization: `Bearer ${this.token}`
|
|
51
242
|
},
|
|
52
|
-
body: signal ? JSON.stringify({ signal }) : void 0
|
|
243
|
+
body: signal ? JSON.stringify({ signal }) : void 0,
|
|
244
|
+
signal: abortSignal
|
|
53
245
|
});
|
|
54
246
|
if (!res.ok) {
|
|
55
247
|
const text = await res.text();
|
|
@@ -94,6 +286,41 @@ var AgentClient = class {
|
|
|
94
286
|
throw new Error(`Agent killShellSession failed (${res.status}): ${text}`);
|
|
95
287
|
}
|
|
96
288
|
}
|
|
289
|
+
async exec(params, opts) {
|
|
290
|
+
const stream = this.run(params, opts?.signal);
|
|
291
|
+
const first = await stream.next();
|
|
292
|
+
if (first.done) throw new Error("Stream ended without 'started' event");
|
|
293
|
+
if (first.value.type !== "started") throw new Error(`Expected 'started' event, got '${first.value.type}'`);
|
|
294
|
+
const cmdId = first.value.id;
|
|
295
|
+
if (!cmdId) throw new Error("'started' event missing 'id' field");
|
|
296
|
+
const startedAt = first.value.ts ? new Date(first.value.ts) : /* @__PURE__ */ new Date();
|
|
297
|
+
return new Command({
|
|
298
|
+
agent: this,
|
|
299
|
+
cmdId,
|
|
300
|
+
startedAt,
|
|
301
|
+
stream,
|
|
302
|
+
signal: opts?.signal,
|
|
303
|
+
onStdout: opts?.onStdout,
|
|
304
|
+
onStderr: opts?.onStderr
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async runCommand(cmdOrParams, args, opts) {
|
|
308
|
+
let params;
|
|
309
|
+
if (typeof cmdOrParams === "string") params = {
|
|
310
|
+
cmd: cmdOrParams,
|
|
311
|
+
args,
|
|
312
|
+
signal: opts?.signal
|
|
313
|
+
};
|
|
314
|
+
else params = cmdOrParams;
|
|
315
|
+
const { signal, onStdout, onStderr, ...runParams } = params;
|
|
316
|
+
const command = await this.exec(runParams, {
|
|
317
|
+
signal,
|
|
318
|
+
onStdout,
|
|
319
|
+
onStderr
|
|
320
|
+
});
|
|
321
|
+
if (params.detached) return command;
|
|
322
|
+
return command.wait({ signal });
|
|
323
|
+
}
|
|
97
324
|
async readFile(path) {
|
|
98
325
|
const res = await fetch(`${this.baseUrl}/files/read`, {
|
|
99
326
|
method: "POST",
|
package/dist/_chunks/connect.mjs
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
|
+
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
+
import "./vm-state.mjs";
|
|
5
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
6
|
+
import { t as ShellSession } from "./shell.mjs";
|
|
7
|
+
import { consola } from "consola";
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
const connectCommand = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "connect",
|
|
12
|
+
description: "Connect to a running VM"
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
vmId: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "VM ID to connect to",
|
|
18
|
+
required: true
|
|
19
|
+
},
|
|
20
|
+
session: {
|
|
21
|
+
type: "string",
|
|
22
|
+
alias: "s",
|
|
23
|
+
description: "Attach to an existing shell session ID",
|
|
24
|
+
required: false
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async run({ args }) {
|
|
28
|
+
const cmdLog = createCommandLogger("connect");
|
|
29
|
+
const paths = vmsanPaths();
|
|
6
30
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
31
|
+
const { state, guestIp, port } = resolveVmState(args.vmId, paths);
|
|
32
|
+
const log = consola.withTag(args.vmId);
|
|
33
|
+
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
34
|
+
log.start("Waiting for agent to become ready...");
|
|
35
|
+
await waitForAgent(guestIp, port);
|
|
36
|
+
log.success("Agent is ready. Connecting via PTY shell...");
|
|
37
|
+
const shell = new ShellSession({
|
|
38
|
+
host: guestIp,
|
|
39
|
+
port,
|
|
40
|
+
token: state.agentToken,
|
|
41
|
+
sessionId: args.session
|
|
42
|
+
});
|
|
43
|
+
const closeInfo = await shell.connect();
|
|
44
|
+
cmdLog.set({
|
|
45
|
+
vmId: args.vmId,
|
|
46
|
+
method: "pty"
|
|
47
|
+
});
|
|
48
|
+
cmdLog.emit();
|
|
49
|
+
if (!closeInfo.sessionDestroyed && shell.sessionId) process.stderr.write(`\n[2mResume this session with:\n vmsan connect ${args.vmId} --session ${shell.sessionId}[0m\n`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
handleCommandError(error, cmdLog);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
10
55
|
}
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
export { waitForAgent as t };
|
|
56
|
+
});
|
|
57
|
+
export { connectCommand as default };
|
package/dist/_chunks/context.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { B as mutuallyExclusiveFlagsError, C as vmNotStoppedError, S as vmNotRunningError, U as VmsanError, _ as chrootNotFoundError, a as noKernelDirError, d as socketTimeoutError, i as noExt4RootfsError, o as noKernelError, p as defaultInterfaceNotFoundError, r as missingBinaryError, s as noRootfsDirError, u as lockTimeoutError, x as vmNotFoundError, y as snapshotNotFoundError } from "./errors.mjs";
|
|
3
3
|
import { c as safeKill, f as toError, i as generateVmId, t as FileVmStateStore } from "./vm-state.mjs";
|
|
4
4
|
import { t as FirecrackerClient } from "./firecracker.mjs";
|
|
5
|
+
import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
|
|
5
6
|
import { createHooks } from "hookable";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
|
-
import { execFileSync, execSync
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
8
9
|
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
10
|
import { consola } from "consola";
|
|
10
11
|
import { randomBytes } from "node:crypto";
|
|
@@ -1988,21 +1989,12 @@ var VMService = class {
|
|
|
1988
1989
|
pid
|
|
1989
1990
|
});
|
|
1990
1991
|
log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
|
|
1991
|
-
if (timeoutMs && pid) {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
`echo "$STATE" | grep -q '"pid":${pid}' || exit 0`,
|
|
1998
|
-
`[ -d /proc/${pid} ] || exit 0`,
|
|
1999
|
-
`grep -q "${vmId}" /proc/${pid}/cmdline 2>/dev/null || exit 0`,
|
|
2000
|
-
`kill ${pid} 2>/dev/null`
|
|
2001
|
-
].join(" && ")], {
|
|
2002
|
-
detached: true,
|
|
2003
|
-
stdio: "ignore"
|
|
2004
|
-
}).unref();
|
|
2005
|
-
}
|
|
1992
|
+
if (timeoutMs && pid) spawnTimeoutKiller({
|
|
1993
|
+
vmId,
|
|
1994
|
+
pid,
|
|
1995
|
+
timeoutMs,
|
|
1996
|
+
stateFile: join(paths.vmsDir, `${vmId}.json`)
|
|
1997
|
+
});
|
|
2006
1998
|
const finalState = this.store.load(vmId);
|
|
2007
1999
|
await hooks.callHook("vm:afterCreate", finalState);
|
|
2008
2000
|
return {
|
package/dist/_chunks/create.mjs
CHANGED
|
@@ -4,10 +4,11 @@ import { i as initVmsanLogger, n as createScopedLogger, t as createCommandLogger
|
|
|
4
4
|
import "./vm-state.mjs";
|
|
5
5
|
import { t as createVmsan } from "./context.mjs";
|
|
6
6
|
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
8
|
+
import { n as waitForAgent } from "./vm-context.mjs";
|
|
7
9
|
import { t as ShellSession } from "./shell.mjs";
|
|
8
10
|
import { a as parseImageReference, t as parseBandwidth } from "./validation.mjs";
|
|
9
11
|
import { n as parseCreateInput, t as buildCreateSummaryLines } from "./summary.mjs";
|
|
10
|
-
import { t as waitForAgent } from "./connect.mjs";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { consola } from "consola";
|
|
13
14
|
import { defineCommand } from "citty";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
-
import
|
|
4
|
+
import "./vm-state.mjs";
|
|
5
5
|
import { t as AgentClient } from "./agent.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
8
8
|
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { consola } from "consola";
|
|
@@ -29,23 +29,8 @@ const downloadCommand = defineCommand({
|
|
|
29
29
|
const cmdLog = createCommandLogger("download");
|
|
30
30
|
const paths = vmsanPaths();
|
|
31
31
|
try {
|
|
32
|
-
const state =
|
|
33
|
-
if (!state) throw vmNotFoundError(args.vmId);
|
|
34
|
-
if (state.status !== "running") {
|
|
35
|
-
consola.error(`VM ${args.vmId} is not running (status: ${state.status})`);
|
|
36
|
-
cmdLog.emit();
|
|
37
|
-
process.exitCode = 1;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!state.agentToken) {
|
|
41
|
-
consola.error("VM has no agent token. Cannot download files without the agent.");
|
|
42
|
-
cmdLog.emit();
|
|
43
|
-
process.exitCode = 1;
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
32
|
+
const { state, guestIp, port } = resolveVmState(args.vmId, paths);
|
|
46
33
|
const log = consola.withTag(args.vmId);
|
|
47
|
-
const guestIp = state.network.guestIp;
|
|
48
|
-
const port = state.agentPort || paths.agentPort;
|
|
49
34
|
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
50
35
|
log.start("Waiting for agent...");
|
|
51
36
|
await waitForAgent(guestIp, port);
|
package/dist/_chunks/errors.mjs
CHANGED
|
@@ -122,10 +122,16 @@ const chrootNotFoundError = (vmId) => new VmError("ERR_VM_CHROOT_NOT_FOUND", {
|
|
|
122
122
|
fix: "The VM must be recreated with 'vmsan create'."
|
|
123
123
|
});
|
|
124
124
|
const networkSlotsExhaustedError = () => new VmError("ERR_VM_NETWORK_SLOTS_EXHAUSTED", { message: "No available network slots (max 255 VMs)" });
|
|
125
|
-
const vmNotRunningError = (vmId) => new VmError("ERR_VM_NOT_RUNNING", {
|
|
125
|
+
const vmNotRunningError = (vmId, currentStatus) => new VmError("ERR_VM_NOT_RUNNING", {
|
|
126
126
|
vmId,
|
|
127
|
-
message: `VM ${vmId} is not running`,
|
|
128
|
-
fix: "The VM must be running
|
|
127
|
+
message: currentStatus ? `VM ${vmId} is not running (current status: ${currentStatus})` : `VM ${vmId} is not running`,
|
|
128
|
+
fix: "The VM must be running. Start it with 'vmsan start <vm-id>'."
|
|
129
|
+
});
|
|
130
|
+
const vmNoAgentTokenError = (vmId) => new VmError("ERR_VM_NO_AGENT_TOKEN", {
|
|
131
|
+
vmId,
|
|
132
|
+
message: `VM ${vmId} has no agent token`,
|
|
133
|
+
why: "The vmsan-agent binary was not found at ~/.vmsan/bin/vmsan-agent when this VM was created.",
|
|
134
|
+
fix: "Install the agent binary into ~/.vmsan/bin/vmsan-agent and recreate the VM with 'vmsan create'."
|
|
129
135
|
});
|
|
130
136
|
const snapshotNotFoundError = (snapshotId) => new VmError("ERR_VM_SNAPSHOT_NOT_FOUND", { message: `Snapshot not found: ${snapshotId}` });
|
|
131
137
|
var FirecrackerApiError = class extends VmsanError {
|
|
@@ -229,4 +235,4 @@ function handleCommandError(error, cmdLog) {
|
|
|
229
235
|
if (error.link) consola.log(` More: ${error.link}`);
|
|
230
236
|
} else consola.error(error instanceof Error ? error.message : String(error));
|
|
231
237
|
}
|
|
232
|
-
export {
|
|
238
|
+
export { invalidDiskSizeRangeError as A, mutuallyExclusiveFlagsError as B, vmNotStoppedError as C, invalidCidrOctetError as D, invalidCidrFormatError as E, invalidImageRefTagError as F, portConflictError as H, invalidIntegerFlagError as I, invalidNetworkPolicyError as L, invalidDomainPatternError as M, invalidDurationError as N, invalidCidrPrefixError as O, invalidImageRefEmptyError as P, invalidPortError as R, vmNotRunningError as S, ValidationError as T, VmsanError as U, policyConflictError as V, chrootNotFoundError as _, noKernelDirError as a, vmNoAgentTokenError as b, TimeoutError as c, socketTimeoutError as d, NetworkError as f, VmError as g, firecrackerApiError as h, noExt4RootfsError as i, invalidDomainError as j, invalidDiskSizeFormatError as k, agentTimeoutError as l, FirecrackerApiError as m, SetupError as n, noKernelError as o, defaultInterfaceNotFoundError as p, missingBinaryError as r, noRootfsDirError as s, handleCommandError as t, lockTimeoutError as u, networkSlotsExhaustedError as v, vmStateNotFoundError as w, vmNotFoundError as x, snapshotNotFoundError as y, invalidRuntimeError as z };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { n as vmsanPaths } from "./paths.mjs";
|
|
2
|
+
import { t as handleCommandError } from "./errors.mjs";
|
|
3
|
+
import { t as createCommandLogger } from "./logger.mjs";
|
|
4
|
+
import "./vm-state.mjs";
|
|
5
|
+
import "./timeout-killer.mjs";
|
|
6
|
+
import { t as AgentClient } from "./agent.mjs";
|
|
7
|
+
import { t as TimeoutExtender } from "./timeout-extender.mjs";
|
|
8
|
+
import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
|
|
9
|
+
import { t as ShellSession } from "./shell.mjs";
|
|
10
|
+
import { consola } from "consola";
|
|
11
|
+
import { defineCommand } from "citty";
|
|
12
|
+
import { isatty } from "node:tty";
|
|
13
|
+
function shellEscape(s) {
|
|
14
|
+
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(s)) return s;
|
|
15
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
16
|
+
}
|
|
17
|
+
function parseEnvFlags(targetCommand) {
|
|
18
|
+
const env = {};
|
|
19
|
+
const argv = process.argv;
|
|
20
|
+
let positionalCount = 0;
|
|
21
|
+
for (let i = 2; i < argv.length; i++) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === "--") break;
|
|
24
|
+
if (!arg.startsWith("-")) {
|
|
25
|
+
positionalCount++;
|
|
26
|
+
if (positionalCount >= 3) break;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let value;
|
|
30
|
+
if (arg === "--env" || arg === "-e") {
|
|
31
|
+
value = argv[i + 1];
|
|
32
|
+
if (value !== void 0) i++;
|
|
33
|
+
} else if (arg.startsWith("--env=")) value = arg.slice(6);
|
|
34
|
+
else if (arg.startsWith("-e=")) value = arg.slice(3);
|
|
35
|
+
if (value !== void 0) {
|
|
36
|
+
const eqIdx = value.indexOf("=");
|
|
37
|
+
if (eqIdx > 0) env[value.slice(0, eqIdx)] = value.slice(eqIdx + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
function buildVmsanPrompt(vmId) {
|
|
43
|
+
return `\\[\\033[1;32m\\]vmsan:${vmId.slice(0, 8)}\\[\\033[0m\\]:\\[\\033[1;34m\\]\\w\\[\\033[0m\\]\\$ `;
|
|
44
|
+
}
|
|
45
|
+
const execCommand = defineCommand({
|
|
46
|
+
meta: {
|
|
47
|
+
name: "exec",
|
|
48
|
+
description: "Execute a command inside a running VM"
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
vmId: {
|
|
52
|
+
type: "positional",
|
|
53
|
+
description: "VM ID, followed by command and arguments",
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
sudo: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
default: false,
|
|
59
|
+
description: "Run with extended privileges (sudo)"
|
|
60
|
+
},
|
|
61
|
+
interactive: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
alias: "i",
|
|
64
|
+
default: false,
|
|
65
|
+
description: "Interactive shell mode (PTY)"
|
|
66
|
+
},
|
|
67
|
+
"no-extend-timeout": {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
default: false,
|
|
70
|
+
description: "Skip timeout extension (interactive only)"
|
|
71
|
+
},
|
|
72
|
+
tty: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
alias: "t",
|
|
75
|
+
default: false,
|
|
76
|
+
description: "Allocate a pseudo-TTY (accepted for compatibility)"
|
|
77
|
+
},
|
|
78
|
+
workdir: {
|
|
79
|
+
type: "string",
|
|
80
|
+
alias: "w",
|
|
81
|
+
description: "Working directory inside the VM"
|
|
82
|
+
},
|
|
83
|
+
env: {
|
|
84
|
+
type: "string",
|
|
85
|
+
alias: "e",
|
|
86
|
+
description: "Environment variable (KEY=VAL), repeatable"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async run({ args }) {
|
|
90
|
+
const cmdLog = createCommandLogger("exec");
|
|
91
|
+
const paths = vmsanPaths();
|
|
92
|
+
try {
|
|
93
|
+
const command = args._[1];
|
|
94
|
+
const commandArgs = args._.slice(2);
|
|
95
|
+
if (!command) {
|
|
96
|
+
consola.error("No command provided. Usage: vmsan exec <vm_id> <command> [...args]");
|
|
97
|
+
cmdLog.emit();
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (args.interactive && !isatty(1)) {
|
|
102
|
+
consola.error("--interactive requires a terminal (TTY).");
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const envVars = parseEnvFlags(command);
|
|
107
|
+
const { state, guestIp, port, store } = resolveVmState(args.vmId, paths);
|
|
108
|
+
consola.debug(`Agent endpoint: ${guestIp}:${port}`);
|
|
109
|
+
const log = consola.withTag(args.vmId);
|
|
110
|
+
log.start("Waiting for agent...");
|
|
111
|
+
await waitForAgent(guestIp, port);
|
|
112
|
+
if (args.interactive) {
|
|
113
|
+
const parts = [];
|
|
114
|
+
parts.push(`export PS1=${shellEscape(buildVmsanPrompt(args.vmId))} TERM=xterm-256color &&`);
|
|
115
|
+
if (args.workdir) parts.push(`cd ${shellEscape(args.workdir)} &&`);
|
|
116
|
+
for (const [key, val] of Object.entries(envVars)) parts.push(`${key}=${shellEscape(val)}`);
|
|
117
|
+
if (args.sudo) parts.push("sudo");
|
|
118
|
+
parts.push(shellEscape(command));
|
|
119
|
+
for (const a of commandArgs) parts.push(shellEscape(a));
|
|
120
|
+
const injectedCmd = parts.join(" ") + "; exit $?\n";
|
|
121
|
+
log.success("Agent is ready. Connecting interactive shell...");
|
|
122
|
+
let extender = null;
|
|
123
|
+
if (!args["no-extend-timeout"] && state.timeoutMs) {
|
|
124
|
+
extender = new TimeoutExtender({
|
|
125
|
+
vmId: args.vmId,
|
|
126
|
+
store,
|
|
127
|
+
paths
|
|
128
|
+
});
|
|
129
|
+
extender.start();
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await new ShellSession({
|
|
133
|
+
host: guestIp,
|
|
134
|
+
port,
|
|
135
|
+
token: state.agentToken,
|
|
136
|
+
initialCommand: injectedCmd
|
|
137
|
+
}).connect();
|
|
138
|
+
} finally {
|
|
139
|
+
extender?.stop();
|
|
140
|
+
}
|
|
141
|
+
cmdLog.set({
|
|
142
|
+
vmId: args.vmId,
|
|
143
|
+
mode: "interactive",
|
|
144
|
+
command,
|
|
145
|
+
args: commandArgs,
|
|
146
|
+
...args["no-extend-timeout"] && { noExtendTimeout: true }
|
|
147
|
+
});
|
|
148
|
+
cmdLog.emit();
|
|
149
|
+
} else {
|
|
150
|
+
consola.debug(`exec: ${command} ${commandArgs.join(" ")}`);
|
|
151
|
+
const agent = new AgentClient(`http://${guestIp}:${port}`, state.agentToken);
|
|
152
|
+
const cmd = args.sudo ? "sudo" : command;
|
|
153
|
+
const runArgs = args.sudo ? [command, ...commandArgs] : commandArgs;
|
|
154
|
+
const params = {
|
|
155
|
+
cmd,
|
|
156
|
+
args: runArgs.length > 0 ? runArgs : void 0,
|
|
157
|
+
cwd: args.workdir || void 0,
|
|
158
|
+
env: Object.keys(envVars).length > 0 ? envVars : void 0
|
|
159
|
+
};
|
|
160
|
+
const ac = new AbortController();
|
|
161
|
+
const onSignal = () => ac.abort();
|
|
162
|
+
process.on("SIGINT", onSignal);
|
|
163
|
+
process.on("SIGTERM", onSignal);
|
|
164
|
+
try {
|
|
165
|
+
const result = await (await agent.exec(params, {
|
|
166
|
+
signal: ac.signal,
|
|
167
|
+
onStdout: (line) => process.stdout.write(line + "\n"),
|
|
168
|
+
onStderr: (line) => process.stderr.write(line + "\n")
|
|
169
|
+
})).wait();
|
|
170
|
+
process.exitCode = result.exitCode;
|
|
171
|
+
if (result.timedOut) consola.error("Command timed out.");
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (!ac.signal.aborted) throw err;
|
|
174
|
+
process.exitCode = 130;
|
|
175
|
+
} finally {
|
|
176
|
+
process.removeListener("SIGINT", onSignal);
|
|
177
|
+
process.removeListener("SIGTERM", onSignal);
|
|
178
|
+
}
|
|
179
|
+
cmdLog.set({
|
|
180
|
+
vmId: args.vmId,
|
|
181
|
+
mode: "non-interactive",
|
|
182
|
+
command,
|
|
183
|
+
args: commandArgs
|
|
184
|
+
});
|
|
185
|
+
cmdLog.emit();
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
handleCommandError(error, cmdLog);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
export { execCommand as default };
|
package/dist/_chunks/list.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
|
|
|
4
4
|
import { d as timeRemaining, l as table, u as timeAgo } from "./vm-state.mjs";
|
|
5
5
|
import { t as createVmsan } from "./context.mjs";
|
|
6
6
|
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { consola } from "consola";
|
|
8
9
|
import { defineCommand } from "citty";
|
|
9
10
|
const STATUS_COLORS = {
|
package/dist/_chunks/network.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "./paths.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { V as policyConflictError, t as handleCommandError } from "./errors.mjs";
|
|
3
3
|
import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
|
|
4
4
|
import "./vm-state.mjs";
|
|
5
5
|
import { t as createVmsan } from "./context.mjs";
|
|
6
6
|
import "./firecracker.mjs";
|
|
7
|
+
import "./timeout-killer.mjs";
|
|
7
8
|
import { d as validateCidr, i as parseDomains, n as parseCidrList, s as parseNetworkPolicy } from "./validation.mjs";
|
|
8
9
|
import { consola } from "consola";
|
|
9
10
|
import { defineCommand } from "citty";
|