virtual-machine 0.0.0-rc1

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/cli.ts ADDED
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ /**
4
+ * RISC-V VM CLI
5
+ *
6
+ * This CLI mirrors the browser VM wiring in `useVM`:
7
+ * - loads a kernel image (ELF or raw binary)
8
+ * - optionally loads a VirtIO block disk image (e.g. xv6 `fs.img`)
9
+ * - can optionally connect to a network relay (WebTransport/WebSocket)
10
+ * - runs the VM in a tight loop
11
+ * - connects stdin → UART input and UART output → stdout
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import yargs from 'yargs';
17
+ import { hideBin } from 'yargs/helpers';
18
+
19
+ // Default relay server URL and cert hash, mirroring the React hook.
20
+ const DEFAULT_RELAY_URL =
21
+ process.env.RELAY_URL || 'https://localhost:4433';
22
+ const DEFAULT_CERT_HASH =
23
+ process.env.RELAY_CERT_HASH || '';
24
+
25
+ /**
26
+ * Create and initialize a Wasm VM instance, mirroring the React `useVM` hook:
27
+ * - initializes the WASM module once via `WasmInternal`
28
+ * - constructs `WasmVm` with the kernel bytes
29
+ * - optionally attaches a VirtIO block device from a disk image
30
+ * - optionally connects to a network relay (WebTransport/WebSocket)
31
+ */
32
+ async function createVm(
33
+ kernelPath: string,
34
+ diskPath?: string,
35
+ options?: {
36
+ network?: boolean;
37
+ relayUrl?: string;
38
+ certHash?: string;
39
+ },
40
+ ) {
41
+ const resolvedKernel = path.resolve(kernelPath);
42
+ const kernelBuf = fs.readFileSync(resolvedKernel);
43
+ const kernelBytes = new Uint8Array(kernelBuf);
44
+
45
+ const { WasmInternal } = await import('./');
46
+ const wasm = await WasmInternal();
47
+ const VmCtor = wasm.WasmVm;
48
+ if (!VmCtor) {
49
+ throw new Error('WasmVm class not found in wasm module');
50
+ }
51
+
52
+ const vm = new VmCtor(kernelBytes);
53
+
54
+ if (diskPath) {
55
+ const resolvedDisk = path.resolve(diskPath);
56
+ const diskBuf = fs.readFileSync(resolvedDisk);
57
+ const diskBytes = new Uint8Array(diskBuf);
58
+
59
+ if (typeof vm.load_disk === 'function') {
60
+ vm.load_disk(diskBytes);
61
+ }
62
+ }
63
+
64
+ // Optional network setup (pre-boot), mirroring `useVM.startVM`.
65
+ if (options?.network) {
66
+ const relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
67
+ const certHash = options.certHash || DEFAULT_CERT_HASH || undefined;
68
+
69
+ try {
70
+ if (typeof (vm as any).connect_webtransport === 'function') {
71
+ (vm as any).connect_webtransport(relayUrl, certHash);
72
+ console.error(
73
+ `[Network] Initiating WebTransport connection to ${relayUrl}`,
74
+ );
75
+ } else if (typeof (vm as any).connect_network === 'function') {
76
+ (vm as any).connect_network(relayUrl);
77
+ console.error(
78
+ `[Network] Initiating WebSocket connection to ${relayUrl}`,
79
+ );
80
+ } else {
81
+ console.error(
82
+ '[Network] No network methods available on VM (rebuild WASM with networking)',
83
+ );
84
+ }
85
+ } catch (err) {
86
+ console.error('[Network] Pre-boot connection failed:', err);
87
+ }
88
+ }
89
+
90
+ return vm;
91
+ }
92
+
93
+ /**
94
+ * Run the VM in a loop and wire stdin/stdout to the UART, similar to the browser loop:
95
+ * - executes a fixed number of instructions per tick
96
+ * - drains the UART output buffer and writes to stdout
97
+ * - feeds raw stdin bytes into the VM's UART input
98
+ */
99
+ function runVmLoop(vm: any) {
100
+ let running = true;
101
+
102
+ const shutdown = (code: number) => {
103
+ if (!running) return;
104
+ running = false;
105
+
106
+ if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
107
+ (process.stdin as any).setRawMode(false);
108
+ }
109
+ process.stdin.pause();
110
+
111
+ process.exit(code);
112
+ };
113
+
114
+ // Handle Ctrl+C via signal as a fallback
115
+ process.on('SIGINT', () => {
116
+ shutdown(0);
117
+ });
118
+
119
+ // Configure stdin → VM UART input
120
+ if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
121
+ (process.stdin as any).setRawMode(true);
122
+ }
123
+ process.stdin.resume();
124
+
125
+ process.stdin.on('data', (chunk) => {
126
+ // In raw mode `chunk` is typically a Buffer; iterate its bytes.
127
+ for (const byte of chunk as any as Uint8Array) {
128
+ // Ctrl+C (ETX) – terminate the VM and exit
129
+ if (byte === 3) {
130
+ shutdown(0);
131
+ return;
132
+ }
133
+
134
+ // Map CR to LF as in the React hook
135
+ if (byte === 13) {
136
+ vm.input(10);
137
+ } else if (byte === 127 || byte === 8) {
138
+ // Backspace
139
+ vm.input(8);
140
+ } else {
141
+ vm.input(byte);
142
+ }
143
+ }
144
+ });
145
+
146
+ const INSTRUCTIONS_PER_TICK = 100_000;
147
+
148
+ const loop = () => {
149
+ if (!running) return;
150
+
151
+ try {
152
+ // Execute a batch of instructions
153
+ for (let i = 0; i < INSTRUCTIONS_PER_TICK; i++) {
154
+ vm.step();
155
+ }
156
+
157
+ // Drain UART output buffer, similar to `useVM`
158
+ const outChunks: string[] = [];
159
+ let limit = 2000;
160
+ let code = typeof vm.get_output === 'function' ? vm.get_output() : undefined;
161
+
162
+ while (code !== undefined && limit-- > 0) {
163
+ const c = Number(code);
164
+
165
+ if (c === 8) {
166
+ // Backspace – move cursor back, erase, move back
167
+ outChunks.push('\b \b');
168
+ } else if (c === 10 || c === 13) {
169
+ outChunks.push('\n');
170
+ } else if (c >= 32 && c <= 126) {
171
+ outChunks.push(String.fromCharCode(c));
172
+ }
173
+
174
+ code = vm.get_output();
175
+ }
176
+
177
+ if (outChunks.length) {
178
+ process.stdout.write(outChunks.join(''));
179
+ }
180
+ } catch (err) {
181
+ console.error('\n[VM] Error while executing:', err);
182
+ shutdown(1);
183
+ return;
184
+ }
185
+
186
+ // Schedule the next tick; run as fast as the event loop allows.
187
+ setImmediate(loop);
188
+ };
189
+
190
+ loop();
191
+ }
192
+
193
+ (yargs(hideBin(process.argv)) as any)
194
+ .command(
195
+ 'run <kernel>',
196
+ 'Runs a RISC-V kernel inside the virtual machine',
197
+ (y: any) =>
198
+ y
199
+ .positional('kernel', {
200
+ type: 'string',
201
+ describe: 'Path to the RISC-V kernel (ELF or raw binary)',
202
+ demandOption: true,
203
+ })
204
+ .option('disk', {
205
+ alias: 'd',
206
+ type: 'string',
207
+ describe: 'Optional path to a VirtIO block disk image (e.g. xv6 fs.img)',
208
+ })
209
+ .option('network', {
210
+ alias: 'n',
211
+ type: 'boolean',
212
+ describe:
213
+ 'Enable network and connect to relay at boot (uses WebTransport/WebSocket)',
214
+ })
215
+ .option('relay-url', {
216
+ alias: 'r',
217
+ type: 'string',
218
+ describe: `Relay URL for WebTransport/WebSocket (default: ${DEFAULT_RELAY_URL})`,
219
+ })
220
+ .option('cert-hash', {
221
+ alias: 'c',
222
+ type: 'string',
223
+ describe:
224
+ 'Optional certificate SHA-256 hash for self-signed TLS (used with WebTransport)',
225
+ }),
226
+ async (argv: any) => {
227
+ const kernelPath = argv.kernel as string;
228
+ const diskPath = (argv.disk ?? undefined) as string | undefined;
229
+ const relayUrlArg = (argv['relay-url'] ?? undefined) as string | undefined;
230
+ const certHashArg = (argv['cert-hash'] ?? undefined) as string | undefined;
231
+ // If user explicitly passes --network or a relay URL, enable networking.
232
+ const enableNetwork =
233
+ (argv.network as boolean | undefined) ?? !!relayUrlArg;
234
+
235
+ try {
236
+ const vm = await createVm(kernelPath, diskPath, {
237
+ network: enableNetwork,
238
+ relayUrl: relayUrlArg,
239
+ certHash: certHashArg,
240
+ });
241
+ runVmLoop(vm);
242
+ } catch (err) {
243
+ console.error('[CLI] Failed to start VM:', err);
244
+ process.exit(1);
245
+ }
246
+ },
247
+ )
248
+ .demandCommand(1, 'You need to specify a command')
249
+ .strict()
250
+ .help()
251
+ .parse();
package/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ import wasmBuffer from "./pkg/riscv_vm_bg.wasm";
2
+
3
+ let loaded: typeof import("./pkg/riscv_vm") | undefined;
4
+
5
+ export async function WasmInternal() {
6
+ if (!loaded) {
7
+ const module = await import("./pkg/riscv_vm");
8
+ const wasmInstance = module.initSync(wasmBuffer);
9
+ await module.default(wasmInstance);
10
+ loaded = module;
11
+ }
12
+ return loaded;
13
+ }
14
+
15
+
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "virtual-machine",
3
+ "version": "0.0.0-rc1",
4
+ "description": "",
5
+ "bin": {
6
+ "uaito-cli": "build/cli.js"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "main": "./build/index.js",
12
+ "module": "./build/index.mjs",
13
+ "types": "./build/index.d.ts",
14
+ "scripts": {
15
+ "build": "rm -rf build && sh build.sh"
16
+ },
17
+ "author": "elribonazo@gmail.com",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "@trust0/ridb-build": "^0.0.21",
21
+ "tsup": "^8.4.0",
22
+ "typescript": "^5.8.3",
23
+ "yargs": "^18.0.0"
24
+ },
25
+ "packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
26
+ "devDependencies": {
27
+ "@esbuild-plugins/node-resolve": "^0.2.2",
28
+ "@types/node": "^24.10.1"
29
+ }
30
+ }