From 7839b5a35992fde0ae92752b8d3ddabf22ee7235 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Tue, 14 Jul 2026 01:16:57 -0400 Subject: [PATCH] Runner: preserve complete guest output on short writes --- examples/run-example-output.ts | 24 ++++++++++++++++++++++++ examples/run-example.ts | 5 +++-- host/test/run-example-output.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 examples/run-example-output.ts create mode 100644 host/test/run-example-output.test.ts diff --git a/examples/run-example-output.ts b/examples/run-example-output.ts new file mode 100644 index 0000000000..8895d9594d --- /dev/null +++ b/examples/run-example-output.ts @@ -0,0 +1,24 @@ +import { writeSync } from "fs"; + +export type WriteBytes = ( + fd: number, + data: Uint8Array, + offset: number, + length: number, +) => number; + +const writeBytes: WriteBytes = (fd, data, offset, length) => + writeSync(fd, data, offset, length, null); + +export function writeAllSync( + fd: number, + data: Uint8Array, + write: WriteBytes = writeBytes, +): void { + let offset = 0; + while (offset < data.byteLength) { + const written = write(fd, data, offset, data.byteLength - offset); + if (written <= 0) throw new Error("short write to guest output sink"); + offset += written; + } +} diff --git a/examples/run-example.ts b/examples/run-example.ts index 93fbbcdbf4..98ca394225 100644 --- a/examples/run-example.ts +++ b/examples/run-example.ts @@ -12,10 +12,11 @@ * npx tsx examples/run-example.ts /path/to/test.wasm */ -import { closeSync, existsSync, openSync, readFileSync, statSync, writeSync } from "fs"; +import { closeSync, existsSync, openSync, readFileSync, statSync } from "fs"; import { resolve, dirname, isAbsolute } from "path"; import { NodeKernelHost } from "../host/src/node-kernel-host"; import { tryResolveBinary } from "../host/src/binary-resolver"; +import { writeAllSync } from "./run-example-output"; import { isWithinRealDirectory } from "./run-example-paths"; const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), ".."); @@ -362,7 +363,7 @@ async function main() { if (guestOutputFd === null) { fallback.write(data); } else { - writeSync(guestOutputFd, data); + writeAllSync(guestOutputFd, data); } }; diff --git a/host/test/run-example-output.test.ts b/host/test/run-example-output.test.ts new file mode 100644 index 0000000000..9b8a83e3bb --- /dev/null +++ b/host/test/run-example-output.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { writeAllSync } from "../../examples/run-example-output"; + +describe("run-example guest output", () => { + it("preserves every byte across short synchronous writes", () => { + const chunks: Uint8Array[] = []; + const offsets: number[] = []; + const input = new Uint8Array([0, 255, 1, 128, 2]); + + writeAllSync(7, input, (_fd, data, offset, length) => { + const written = Math.min(2, length); + offsets.push(offset); + chunks.push(new Uint8Array(data.subarray(offset, offset + written))); + return written; + }); + + expect(offsets).toEqual([0, 2, 4]); + expect(new Uint8Array(Buffer.concat(chunks))).toEqual(input); + }); + + it("fails instead of spinning when a write makes no progress", () => { + expect(() => + writeAllSync(7, new Uint8Array([1]), () => 0), + ).toThrow("short write to guest output sink"); + }); +});