Skip to content
Merged
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
19 changes: 17 additions & 2 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<NodeJS.WriteStream>();
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();
Expand Down Expand Up @@ -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); };
Expand Down
20 changes: 20 additions & 0 deletions test/pipe/Pipe.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
16 changes: 15 additions & 1 deletion test/programs.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(" ") : [];
Expand Down
Loading