diff --git a/src/node.ts b/src/node.ts index b182677..39099d3 100644 --- a/src/node.ts +++ b/src/node.ts @@ -14,6 +14,7 @@ import { JVM, LM_CLASS, LM_JAR } from "./jvm.ts"; import { hostNatives } from "./hostnatives.ts"; import { ImageBackend } from "./image.ts"; import { NodeNet } from "./net-node.ts"; +import { PendingThrow } from "./thread.ts"; import type { NetBackend } from "./sockets.ts"; // A host directory exposed read-only (or writable) inside the VFS. @@ -194,6 +195,20 @@ function exposeGc() { if (gc) g.gc = gc; } +// Once the reader goes away (a closed pipe), writes fail with an IOException as +// on HotSpot; without a listener, node exits on the stream's error event. +const broken = new Set(); +function hostOut(s: NodeJS.WriteStream) { + if (!s.listeners("error").includes(onOutError)) s.on("error", onOutError); + return (b: Uint8Array) => { + if (broken.has(s)) throw new PendingThrow("java/io/IOException", "Broken pipe"); + s.write(b); + }; +} +function onOutError(this: NodeJS.WriteStream) { + broken.add(this); +} + function nodeVm(o: NodeRunOptions): JVM { exposeGc(); const vfs = new VFS(); @@ -232,8 +247,8 @@ function nodeVm(o: NodeRunOptions): JVM { const mod = await import(pathToFileURL(p).href); Object.assign(natives, nativeTable(mod)); }, - stdout: o.stdout ?? ((b) => process.stdout.write(b)), - stderr: o.stderr ?? ((b) => process.stderr.write(b)), + stdout: o.stdout ?? hostOut(process.stdout), + stderr: o.stderr ?? hostOut(process.stderr), stdin: o.input ? fixedInput(o.input) : () => new Promise((resolve) => { const onData = (d: Buffer) => { cleanup(); resolve(new Uint8Array(d)); }; const onEnd = () => { cleanup(); resolve(null); }; diff --git a/test/pipe/Pipe.java b/test/pipe/Pipe.java new file mode 100644 index 0000000..72c0bc2 --- /dev/null +++ b/test/pipe/Pipe.java @@ -0,0 +1,20 @@ +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; + +// Writes to stdout after its reader has gone, as under `| head -1`. +public class Pipe { + public static void main(String[] args) throws Exception { + for (int i = 0; i < 20000; i++) System.out.println("line " + i); + // java-web learns of the closed pipe on node's next event loop turn + Thread.sleep(200); + System.out.println("after"); + System.err.println("checkError " + System.out.checkError()); + try (var raw = new FileOutputStream(FileDescriptor.out)) { + raw.write('x'); + System.err.println("raw write ok"); + } catch (IOException e) { + System.err.println("raw " + e.getMessage()); + } + } +} diff --git a/test/programs.test.ts b/test/programs.test.ts index 4c848b7..54dcc2d 100644 --- a/test/programs.test.ts +++ b/test/programs.test.ts @@ -1,7 +1,7 @@ // Compiles each test/java/*.java and checks that java-web prints exactly // what HotSpot prints, with the same exit code. -import { execFileSync, spawnSync } from "node:child_process"; +import { execFileSync, spawn, spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -64,6 +64,20 @@ test("System.gc collects under the command", { skip: !files.includes("Refs.java" assert.deepEqual({ stdout: got.stdout, code: got.status }, { stdout: ref.stdout, code: ref.status }); }); +test("stdout closed by its reader", async () => { + const classes = join(root, "build", "test-pipe"); + execFileSync(hotspot("javac"), ["-d", classes, join(root, "test", "pipe", "Pipe.java")]); + // reads the first chunk, then closes the pipe + const run = (cmd: string, args: string[]) => new Promise<{ stderr: string; code: number | null }>((resolve) => { + const p = spawn(cmd, [...args, "-cp", classes, "Pipe"], { stdio: ["ignore", "pipe", "pipe"] }); + let stderr = ""; + p.stderr.on("data", (d) => (stderr += d)); + p.stdout.once("data", () => p.stdout.destroy()); + p.on("close", (code) => resolve({ stderr, code })); + }); + assert.deepEqual(await run(process.execPath, [join(root, "src", "node.ts")]), await run(hotspot("java"), [])); +}); + function flags(file: string, tool: string): string[] { const m = new RegExp(`^// ${tool}: (.*)$`, "m").exec(readFileSync(join(src, file), "utf8")); return m ? m[1].split(" ") : [];