Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tools/wasm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 0 additions & 2 deletions tools/wasm/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@
WebglAddon,
} from "./vendor/xterm/dist.js";
import {
BlockDevice,
ConsoleDevice,
EntropyDevice,
Machine,
Expand Down Expand Up @@ -179,7 +178,6 @@
devices: [
new ConsoleDevice(stdin, stdout),
new EntropyDevice(),
new BlockDevice(new Uint8Array(8 * 1024 * 1024)),
],
initcpio: new Uint8Array(initcpio),
});
Expand Down
113 changes: 90 additions & 23 deletions tools/wasm/run.js
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -32,23 +64,58 @@ options:
-m, --memory <number> Amount of memory to allocate in MiB (default: ${defaultMemory})
-i, --initcpio <string> Path to the initramfs to boot
-j, --cpus <number> 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 <string> 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),
});

Expand Down
2 changes: 1 addition & 1 deletion tools/wasm/src/devicetree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 8 additions & 3 deletions tools/wasm/src/index.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
87 changes: 60 additions & 27 deletions tools/wasm/src/virtio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -229,21 +219,32 @@ const BlockDeviceStatus = {
UNSUPP: 2,
} as const;

type MaybePromise<T> = T | Promise<T>;
export interface BlockDeviceStorage {
read(offset: number, length: number): MaybePromise<Uint8Array>;
write?(offset: number, data: Uint8Array): MaybePromise<number>;
flush?(): MaybePromise<void>;
capacity: number;
}

export class BlockDevice extends VirtioDevice<BlockDeviceConfig> {
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];
Expand All @@ -257,30 +258,62 @@ export class BlockDevice extends VirtioDevice<BlockDeviceConfig> {
`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);
Expand Down
Loading