From f5dc391083061c28a499d24f4e8d675995410f55 Mon Sep 17 00:00:00 2001 From: Thomas Stokes Date: Mon, 10 Nov 2025 19:35:17 +0800 Subject: [PATCH] tools/wasm: virtio disk: implement write --- tools/wasm/Makefile | 2 +- tools/wasm/public/index.html | 2 - tools/wasm/run.js | 113 ++++++++++++++++++++++++++++------- tools/wasm/src/devicetree.ts | 2 +- tools/wasm/src/index.ts | 11 +++- tools/wasm/src/virtio.ts | 87 ++++++++++++++++++--------- 6 files changed, 160 insertions(+), 57 deletions(-) diff --git a/tools/wasm/Makefile b/tools/wasm/Makefile index 14f77f3bf45aaa..0475ef38196e4c 100644 --- a/tools/wasm/Makefile +++ b/tools/wasm/Makefile @@ -20,5 +20,5 @@ _kernel: dist/index.js: _kernel $(shell find src -type f) $(Q)$(ESBUILD) --bundle src/index.ts --outdir=dist --format=esm --splitting --sourcemap --loader:.wasm=file --loader:.img=file --loader:.cpio=binary -dist/index.d.ts: _kernel $(shell find src -type f -name '*.ts') +dist/index.d.ts: $(shell find src -type f -name '*.ts') $(Q)$(TSC) diff --git a/tools/wasm/public/index.html b/tools/wasm/public/index.html index 33991c4684d8bc..999c40112af78d 100644 --- a/tools/wasm/public/index.html +++ b/tools/wasm/public/index.html @@ -112,7 +112,6 @@ WebglAddon, } from "./vendor/xterm/dist.js"; import { - BlockDevice, ConsoleDevice, EntropyDevice, Machine, @@ -179,7 +178,6 @@ devices: [ new ConsoleDevice(stdin, stdout), new EntropyDevice(), - new BlockDevice(new Uint8Array(8 * 1024 * 1024)), ], initcpio: new Uint8Array(initcpio), }); diff --git a/tools/wasm/run.js b/tools/wasm/run.js index 455c8475143134..243b6817d25b6d 100755 --- a/tools/wasm/run.js +++ b/tools/wasm/run.js @@ -1,28 +1,60 @@ -#!/usr/bin/env -S deno run --allow-read +#!/usr/bin/env -S deno run --allow-all import { BlockDevice, ConsoleDevice, EntropyDevice, Machine, } from "./dist/index.js"; -import { parseArgs } from "jsr:@std/cli@1/parse-args"; +import { parseArgs } from "node:util"; import { assert } from "./src/util.ts"; const defaultMemory = navigator.hardwareConcurrency > 16 ? 256 : 128; -const args = parseArgs(Deno.args, { - string: ["cmdline"], - boolean: ["help"], - default: { - cmdline: "", - memory: defaultMemory, - cpus: navigator.hardwareConcurrency, - initcpio: import.meta.url - .replace("run.js", "initramfs.cpio") - .replace("file://", ""), +const args = parseArgs({ + allowNegative: true, + options: { + cmdline: { + short: "c", + type: "string", + default: "", + }, + memory: { + short: "m", + type: "string", + default: defaultMemory.toString(), + }, + initcpio: { + short: "i", + type: "string", + default: import.meta.url + .replace("run.js", "initramfs.cpio") + .replace("file://", ""), + }, + cpus: { + short: "j", + type: "string", + default: navigator.hardwareConcurrency.toString(), + }, + help: { + short: "h", + type: "boolean", + default: false, + }, + console: { + type: "boolean", + default: true, + }, + entropy: { + type: "boolean", + default: true, + }, + disk: { + type: "string", + default: [], + multiple: true, + }, }, - alias: { cmdline: "c", memory: "m", initcpio: "i", cpus: "j", help: "h" }, -}); +}).values; if (args.help) { console.log(`usage: run.ts [options] @@ -32,23 +64,58 @@ options: -m, --memory Amount of memory to allocate in MiB (default: ${defaultMemory}) -i, --initcpio Path to the initramfs to boot -j, --cpus Number of CPUs to use (default: number of CPUs on the machine) + --no-console Don't attach a console device + --no-entropy Don't attach an entropy device + --disk Path to a disk image to use (can be specified multiple times) -h, --help Show this help message `); Deno.exit(0); } -assert(typeof args.cpus === "number", "cpus must be a number"); -assert(typeof args.memory === "number", "memory must be a number"); +assert(/^\d+$/.test(args.cpus), "cpus must be an integer"); +assert(/^\d+$/.test(args.memory), "memory must be an integer"); + +const devices = []; + +if (args.console) { + devices.push(new ConsoleDevice(Deno.stdin.readable, Deno.stdout.writable)); +} + +if (args.entropy) { + devices.push(new EntropyDevice()); +} + +for (const disk of args.disk) { + let readonly = false; + const file = await Deno.open(disk, { read: true, write: true }).catch(() => { + readonly = true; + return Deno.open(disk, { read: true }); + }); + const { size } = await file.stat(); + + devices.push( + new BlockDevice({ + read: async (offset, length) => { + const array = new Uint8Array(length); + await file.seek(offset, Deno.SeekMode.Start); + const n = (await file.read(array)) ?? 0; + return array.subarray(0, n); + }, + write: readonly ? undefined : async (offset, data) => { + await file.seek(offset, Deno.SeekMode.Start); + return await file.write(data); + }, + flush: () => file.sync(), + capacity: size, + }), + ); +} const machine = new Machine({ cmdline: args.cmdline, - memoryMib: args.memory, - cpus: args.cpus, - devices: [ - new ConsoleDevice(Deno.stdin.readable, Deno.stdout.writable), - new EntropyDevice(), - new BlockDevice(new Uint8Array(8 * 1024 * 1024)), - ], + memoryMib: parseInt(args.memory), + cpus: parseInt(args.cpus), + devices, initcpio: await Deno.readFile(args.initcpio), }); diff --git a/tools/wasm/src/devicetree.ts b/tools/wasm/src/devicetree.ts index dec8252cf78a96..2012642c5833f6 100644 --- a/tools/wasm/src/devicetree.ts +++ b/tools/wasm/src/devicetree.ts @@ -110,7 +110,7 @@ export function generate_devicetree(tree: DeviceTreeNode, { ); (strings[name] ??= []).push(property); - let value: ArrayBuffer; + let value: ArrayBufferLike; switch (typeof prop) { case "number": value = new Uint32Array(1).buffer; diff --git a/tools/wasm/src/index.ts b/tools/wasm/src/index.ts index fc16de55cd5893..ee8a999478192a 100644 --- a/tools/wasm/src/index.ts +++ b/tools/wasm/src/index.ts @@ -1,17 +1,22 @@ import initramfs from "./build/initramfs_data.cpio"; import sections from "./build/sections.json" with { type: "json" }; -import vmlinuxUrl from "./build/vmlinux.wasm"; +import vmlinux_url from "./build/vmlinux.wasm"; import { type DeviceTreeNode, generate_devicetree } from "./devicetree.ts"; import { assert, EventEmitter, get_script_path, unreachable } from "./util.ts"; import { virtio_imports, VirtioDevice } from "./virtio.ts"; import { type Imports, type Instance, kernel_imports } from "./wasm.ts"; import type { InitMessage, WorkerMessage } from "./worker.ts"; -export { BlockDevice, ConsoleDevice, EntropyDevice } from "./virtio.ts"; +export { + BlockDevice, + type BlockDeviceStorage, + ConsoleDevice, + EntropyDevice, +} from "./virtio.ts"; const worker_url = get_script_path(() => import("./worker.ts"), import.meta); -const vmlinux_response = fetch(new URL(vmlinuxUrl, import.meta.url)); +const vmlinux_response = fetch(new URL(vmlinux_url, import.meta.url)); const vmlinux_promise = "compileStreaming" in WebAssembly ? WebAssembly.compileStreaming(vmlinux_response) : vmlinux_response.then((r) => r.arrayBuffer()).then(WebAssembly.compile); diff --git a/tools/wasm/src/virtio.ts b/tools/wasm/src/virtio.ts index 31a4d5cb05022e..ae6ca763c32581 100644 --- a/tools/wasm/src/virtio.ts +++ b/tools/wasm/src/virtio.ts @@ -24,16 +24,6 @@ const DescriptorFlags = { USED: 1 << 15, } as const; -function formatDescFlags(flags: number) { - const f = []; - if (flags & DescriptorFlags.NEXT) f.push("NEXT"); - if (flags & DescriptorFlags.WRITE) f.push("WRITE"); - if (flags & DescriptorFlags.INDIRECT) f.push("INDIRECT"); - if (flags & DescriptorFlags.AVAIL) f.push("AVAIL"); - if (flags & DescriptorFlags.USED) f.push("USED"); - return f.join(" "); -} - class VirtqDescriptor extends Struct({ addr: U64LE, len: U32LE, @@ -229,21 +219,32 @@ const BlockDeviceStatus = { UNSUPP: 2, } as const; +type MaybePromise = T | Promise; +export interface BlockDeviceStorage { + read(offset: number, length: number): MaybePromise; + write?(offset: number, data: Uint8Array): MaybePromise; + flush?(): MaybePromise; + capacity: number; +} + export class BlockDevice extends VirtioDevice { ID = 2; config_bytes = new Uint8Array(BlockDeviceConfig.size); config = new BlockDeviceConfig(this.config_bytes); - #storage: Uint8Array; + #storage: BlockDeviceStorage; - constructor(storage: Uint8Array) { + constructor(storage: BlockDeviceStorage) { super(); this.#storage = storage; - this.features |= BlockDeviceFeatures.FLUSH; - this.config.capacity = BigInt(this.#storage.byteLength / 512); + + if (storage.flush) this.features |= BlockDeviceFeatures.FLUSH; + if (!storage.write) this.features |= BlockDeviceFeatures.RO; + + this.config.capacity = BigInt(storage.capacity / 512); } - override notify(vq: number) { + override async notify(vq: number) { assert(vq === 0); const queue = this.vqs[vq]; @@ -257,30 +258,62 @@ export class BlockDevice extends VirtioDevice { `header size is ${header.array.byteLength}`, ); assert(data, "data must exist"); - assert(status && status.writable, "status must be writable"); - assert( - status.array.byteLength === 1, - `status size is ${status.array.byteLength}`, - ); assert(!trailing, "too many descriptors"); const request = new BlockDeviceRequest(header.array); + function set_status(value: number) { + assert(status && status.writable, "status must be writable"); + assert( + status.array.byteLength === 1, + `status size is ${status.array.byteLength}`, + ); + status.array[0] = value; + } + let n = 0; switch (request.type) { case BlockDeviceRequestType.IN: { assert(data.writable, "data must be writable when IN"); - const start = Number(request.sector) * 512; - let end = start + data.array.byteLength; - if (end >= this.#storage.length) end = this.#storage.length - 1; - data.array.set(this.#storage.subarray(start, end)); - n = end - start; - status.array[0] = BlockDeviceStatus.OK; + const arr = await this.#storage.read( + Number(request.sector) * 512, + data.array.byteLength, + ); + data.array.set(arr); + n = arr.byteLength; + set_status(BlockDeviceStatus.OK); + break; + } + case BlockDeviceRequestType.OUT: { + if (!this.#storage.write) { + set_status(BlockDeviceStatus.UNSUPP); + break; + } + assert(!data.writable, "data must be readonly when OUT"); + n = await this.#storage.write( + Number(request.sector) * 512, + data.array, + ); + set_status(BlockDeviceStatus.OK); + break; + } + case BlockDeviceRequestType.FLUSH: { + if (!this.#storage.flush) { + set_status(BlockDeviceStatus.UNSUPP); + break; + } + await this.#storage.flush(); + set_status(BlockDeviceStatus.OK); + break; + } + case BlockDeviceRequestType.GET_ID: { + console.log("GET_ID"); + set_status(BlockDeviceStatus.OK); break; } default: console.error("unknown request type", request.type); - status.array[0] = BlockDeviceStatus.UNSUPP; + set_status(BlockDeviceStatus.UNSUPP); } chain.release(n);