Skip to content
Merged
2 changes: 2 additions & 0 deletions src/jvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface JvmOptions {
loadNativeLibrary?: (path: string) => Promise<void>;
// told when Java closes a file it wrote
fileClosed?: (path: string) => void;
// told when Java closes descriptor 1 or 2, its standard output or error
closeOutput?: (fd: number) => void;
// runs before the first OutOfMemoryError the VM throws is created
onOutOfMemory?: (message: string | null) => void;
// log VM internals to the host console
Expand Down
4 changes: 2 additions & 2 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ export function closeFd(t: JThread, fd: number) {
if (f.writable && f.path) t.vm.opts.fileClosed?.(f.path);
// the descriptor is closed either way, as with close(2)
if (!ok) throw writeFailed(t, f.path);
}
} else if (fd === 1 || fd === 2) t.vm.opts.closeOutput?.(fd);
}

// Open files follow their inode: a rename moves their path, an unlink drops it.
Expand Down Expand Up @@ -687,7 +687,7 @@ export const natives: Natives = {
const fd = t.vm.getField(fdObj, "java/io/FileDescriptor", "fd");
// closed even when close reports a failed write
t.vm.setField(fdObj, "java/io/FileDescriptor", "fd", -1);
if (fd > 2) closeFd(t, fd);
if (fd > 0) closeFd(t, fd);
},

[`${FOS}.open0(Ljava/lang/String;Z)V`]: (t, [self, path, append]) =>
Expand Down
34 changes: 34 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isAssertionFlag, parseAssertions } from "./assertions.ts";
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import { isMainThread } from "node:worker_threads";
import * as path from "node:path";
import { deserialize, serialize, setFlagsFromString } from "node:v8";
import { runInNewContext } from "node:vm";
Expand Down Expand Up @@ -400,13 +401,42 @@ 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 (closed.has(s)) throw new PendingThrow("java/io/IOException", "Bad file descriptor");
if (broken.has(s)) throw new PendingThrow("java/io/IOException", "Broken pipe");
s.write(b);
};
}
function onOutError(this: NodeJS.WriteStream) {
broken.add(this);
}
// Java closing descriptor 1 or 2 closes the host's, as in HotSpot, so a reader sees end of file.
// The null device takes the number, so no file opened later gets the stream's writes.
const closed = new Set<NodeJS.WriteStream>();
function closeHostOut(s: NodeJS.WriteStream, fd: number) {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
// a Worker's stdout and stderr are not descriptors 1 and 2, which belong to the whole process
if (!isMainThread || closed.has(s)) return;
closed.add(s);
let done = false;
const close = () => {
if (done) return;
done = true;
try {
fs.closeSync(fd);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// open takes the lowest free number: fill any free one below fd, then let those go
const below: number[] = [];
for (let nul = fs.openSync(os.devNull, "w"); nul !== fd; nul = fs.openSync(os.devNull, "w")) {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
below.push(nul);
if (nul > fd) break;
}
for (const nul of below) fs.closeSync(nul);
} catch { /* already closed */ }
};
if (s.destroyed || s.writableEnded) return close();
// runs once the writes before it are done
s.write("", close);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
s.once("error", close);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
s.once("close", close);
}

// What HotSpot does on the first OutOfMemoryError: it prints the commands and
// runs each through /bin/sh, with %p the process id, before throwing.
Expand Down Expand Up @@ -600,6 +630,10 @@ export function nodeVm(o: LauncherOptions): JVM {
},
stdout: o.stdout ?? hostOut(process.stdout),
stderr: o.stderr ?? hostOut(process.stderr),
closeOutput: (fd) => {
if (fd === 1 && !o.stdout) closeHostOut(process.stdout, 1);
else if (fd === 2 && !o.stderr) closeHostOut(process.stderr, 2);
},
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
6 changes: 2 additions & 4 deletions src/process-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,8 @@ function execFailure(error: NodeJS.ErrnoException): string {
return `Exec failed, error: ${-error.errno!} (${text}) `;
}

// A child's output waits in memory up to this many bytes before the child is held back. More than a
// pipe holds, since a java-web child cannot close its host stdout: a parent that reads stdout to its
// end before stderr would otherwise wait on a child held back on stderr.
const HELD = 1 << 24;
// A child's output waits in memory up to this many bytes, what a Linux pipe holds, before the child is held back
const HELD = 1 << 16;

class Pipe implements ProcessPipe {
private chunks: Uint8Array[] = [];
Expand Down
47 changes: 47 additions & 0 deletions test/java/CloseStdout.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;

// A child that closes System.out (or System.err) ends that stream at once: the parent reads it to
// the end first, while the child writes more to the other stream than a pipe holds.
public class CloseStdout {
public static void main(String[] args) throws Exception {
if (args.length > 0) {
boolean out = args[0].equals("out");
PrintStream closing = out ? System.out : System.err, other = out ? System.err : System.out;
closing.print("before close");
closing.close();
byte[] line = new byte[1023];
Arrays.fill(line, (byte) 'e');
for (int i = 0; i < 1024; i++) {
other.write(line);
other.write('\n');
}
other.flush();
try {
new FileOutputStream(out ? FileDescriptor.out : FileDescriptor.err).write('x');
other.println("write after close worked");
} catch (IOException e) {
other.println("write after close: " + e.getMessage());
}
return;
}
String java = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
for (String which : new String[] {"out", "err"}) {
Process p = new ProcessBuilder(java, "-cp", System.getProperty("java.class.path"), "CloseStdout", which).start();
InputStream closed = which.equals("out") ? p.getInputStream() : p.getErrorStream();
InputStream other = which.equals("out") ? p.getErrorStream() : p.getInputStream();
String first = new String(closed.readAllBytes());
byte[] rest = other.readAllBytes();
System.out.println(which + " closed: " + first);
System.out.println("other bytes: " + rest.length);
String text = new String(rest);
System.out.println(text.substring(text.lastIndexOf('\n', text.length() - 2) + 1).trim());
System.out.println("exit: " + p.waitFor());
}
}
}
1 change: 1 addition & 0 deletions test/jtreg/passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ java/lang/RuntimeTests/exec/ConcurrentRead.java
java/lang/RuntimeTests/exec/Duped.java
java/lang/RuntimeTests/exec/ExitValue.java
java/lang/RuntimeTests/exec/LotsOfDestroys.java
java/lang/RuntimeTests/exec/LotsOfOutput.java
java/lang/RuntimeTests/loadLibrary/LoadLibraryTest.java
java/lang/RuntimeTests/shutdown/ShutdownHooks.java
java/lang/RuntimeTests/shutdown/ShutdownInterruptedMain.java
Expand Down
Loading