Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9980161
Add preliminary Node child process support.
Project516 Nov 5, 2026
3978cc9
Preserve child output until streams close.
Project516 Nov 5, 2026
82715e4
Connect child pipelines and track process exit independently.
Project516 Nov 5, 2026
c5e882d
Keep ordinary file reads synchronous.
Project516 Nov 5, 2026
e310b06
Look up process parents on Linux.
Project516 Nov 5, 2026
56e7f42
Check that missing programs raise an exception.
Project516 Nov 5, 2026
64c44a2
Merge stderr into piped stdout with file redirects.
Project516 Nov 5, 2026
73a354b
Enumerate Linux processes for process handles.
Project516 Nov 5, 2026
077d89f
Report process start times and information from procfs.
Project516 Nov 5, 2026
d7a4782
Keep child enumeration available without procfs.
Project516 Nov 5, 2026
234bac3
Report child CPU time from procfs.
Project516 Nov 5, 2026
e2a49aa
Check process information and merged output ordering.
Project516 Nov 5, 2026
f80c901
Run newly passing OpenJDK process tests.
Project516 Nov 5, 2026
a659057
Preserve the order of merged child output.
Project516 Nov 5, 2026
f07d198
Support descendant termination and JDK image input redirects.
Project516 Nov 5, 2026
999150b
Merge remote-tracking branch 'origin/master' into p516/processes
Project516 Nov 5, 2026
4777990
Ignore -X and --add- options when java starts a child VM, as the java…
Project516 Nov 5, 2026
2347c74
Document child processes under Node
Project516 Nov 5, 2026
43c95ab
Guard inherited child output and run the process tests on Linux only
Project516 Nov 5, 2026
99f4f3f
Test inherited child output order and explain the exit wait
Project516 Nov 5, 2026
c522378
Wait for drain when forwarding into a child's input
Project516 Nov 5, 2026
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ See [docs/node.md](docs/node.md).
`BufferedImage`, with HotSpot's pixels except for text.
- Sockets: real ones under Node; in the browser, loopback inside the VM and
`http:`/`https:` through `fetch`.
- Not yet: AWT and Swing windows, child processes, IPv6.
- Child processes under Node, through the host's own programs.
- Not yet: AWT and Swing windows, child processes in the browser, IPv6.

Details: [docs/support.md](docs/support.md).

Expand Down
7 changes: 6 additions & 1 deletion docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ java-web targets headless Java: console apps, libraries and javac.
- `Deflater` writes valid DEFLATE streams using stored blocks. Compression
levels and strategies currently do not reduce output size; compressed bytes
and compression ratios differ from HotSpot's zlib output.
- Child processes (`ProcessBuilder`, `Runtime.exec`, `ProcessHandle`) work
under Node and run the host's programs. Starting `java` starts another
java-web VM on the same image. A child cannot use a directory that exists only
in the VM, such as `/jdk`, as its working directory. `ProcessHandle` reads
other processes from Linux's `/proc`; elsewhere it sees only its own children.
- Not supported: GUI apps (AWT and Swing windows), `ConvolveOp`, child
processes, and native libraries other than JS natives.
processes in the browser, and native libraries other than JS natives.

## Locales

Expand Down
53 changes: 49 additions & 4 deletions src/natives/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import { closeSockFd } from "../sockets.ts";
import { nonNull, u8 } from "./util.ts";
import type { Natives } from "./util.ts";

export interface ProcessPipe {
read(dst: Uint8Array, off: number, len: number): Promise<number>;
write(bytes: Uint8Array): Promise<string | null>;
available(): number;
close(): void;
}

function pipes(t: JThread): Map<number, ProcessPipe> {
let map = t.vm.state.get("processPipes") as Map<number, ProcessPipe> | undefined;
if (!map) t.vm.state.set("processPipes", (map = new Map()));
return map;
}

export function addProcessPipe(t: JThread, pipe: ProcessPipe): number {
const fd = newFd(t);
pipes(t).set(fd, pipe);
return fd;
}

export function processPipe(t: JThread, fd: number): ProcessPipe | undefined {
return (t.vm.state.get("processPipes") as Map<number, ProcessPipe> | undefined)?.get(fd);
}

// A file's bytes, shared by every descriptor open on it, as an inode is.
interface Contents {
data: Uint8Array;
Expand All @@ -20,6 +43,7 @@ export class OpenFile {
// "" once the file is unlinked: it lives until closed, but no path reaches it
path: string;
pos = 0;
append = false;
writable: boolean;
contents: Contents;
ino?: number;
Expand Down Expand Up @@ -181,7 +205,7 @@ export function openFile(t: JThread, path: string, write: boolean, truncate: boo
}
const f = new OpenFile(abs, data, write, shared);
if (truncate && st) f.setLength(0);
if (append) f.pos = f.size;
if (append) { f.pos = f.size; f.append = true; }
const tab = table(t);
const fd = tab.next++;
tab.files.set(fd, f);
Expand Down Expand Up @@ -246,6 +270,12 @@ export function newFd(t: JThread): number {

export function closeFd(t: JThread, fd: number) {
if (closeSockFd(t.vm.state, t.vm.net, fd)) return;
const pipe = processPipe(t, fd);
if (pipe) {
pipes(t).delete(fd);
pipe.close();
return;
}
const tab = table(t);
const f = tab.files.get(fd);
if (f) {
Expand Down Expand Up @@ -310,9 +340,19 @@ function setFd(t: JThread, stream: JObject, owner: string, fd: number) {
t.vm.setField(fdObj, "java/io/FileDescriptor", "fd", fd);
}

function* readProcessPipe(t: JThread, pipe: ProcessPipe, dst: Uint8Array, off: number, len: number): NativeGen {
return yield t.block(pipe.read(dst, off, len));
}

function* writeProcessPipe(t: JThread, pipe: ProcessPipe, bytes: Uint8Array): NativeGen {
const error: string | null = yield t.block(pipe.write(bytes));
if (error) throw new PendingThrow("java/io/IOException", error);
}

function writeOut(t: JThread, fd: number, bytes: Uint8Array) {
if (fd === 1) t.vm.opts.stdout(bytes.slice());
else if (fd === 2) t.vm.opts.stderr(bytes.slice());
else if (processPipe(t, fd)) return writeProcessPipe(t, processPipe(t, fd)!, bytes);
else {
const f = fileOf(t, fd);
if (!f.writable) throw new PendingThrow("java/io/IOException", "Bad file descriptor");
Expand Down Expand Up @@ -448,7 +488,7 @@ export const natives: Natives = {
[`${FOS}.writeBytes([BIIZ)V`]: (t, [self, b, off, len]) => {
const a = nonNull<JArray>(b);
if (off < 0 || len < 0 || off + len > a.a.length) throw new PendingThrow("java/lang/IndexOutOfBoundsException");
writeOut(t, fdOf(t, self, FOS), u8(a, off, len));
return writeOut(t, fdOf(t, self, FOS), u8(a, off, len));
},
[`${FOS}.write(IZ)V`]: (t, [self, b]) => writeOut(t, fdOf(t, self, FOS), Uint8Array.of(b & 0xff)),

Expand All @@ -459,12 +499,15 @@ export const natives: Natives = {
if (len === 0) return 0;
const fd = fdOf(t, self, FIS);
if (fd === 0) return readStdin(t, u8(a), off, len);
const pipe = processPipe(t, fd);
if (pipe) return readProcessPipe(t, pipe, u8(a), off, len);
return fileOf(t, fd).read(u8(a), off, len);
},
[`${FIS}.read0()I`]: function* (t, [self]): NativeGen {
const fd = fdOf(t, self, FIS);
const one = new Uint8Array(1);
const n = fd === 0 ? yield* readStdin(t, one, 0, 1) : fileOf(t, fd).read(one, 0, 1);
const pipe = processPipe(t, fd);
const n = fd === 0 ? yield* readStdin(t, one, 0, 1) : pipe ? yield t.block(pipe.read(one, 0, 1)) : fileOf(t, fd).read(one, 0, 1);
return n <= 0 ? -1 : one[0];
},
[`${FIS}.length0()J`]: (t, [self]) => BigInt(fileOf(t, fdOf(t, self, FIS)).size),
Expand All @@ -479,10 +522,12 @@ export const natives: Natives = {
[`${FIS}.available0()I`]: (t, [self]) => {
const fd = fdOf(t, self, FIS);
if (fd === 0) return (t.vm.state.get("stdin") as Stdin | undefined)?.buf.length ?? 0;
const pipe = processPipe(t, fd);
if (pipe) return pipe.available();
const f = fileOf(t, fd);
return Math.min(Math.max(0, f.size - f.pos), 0x7fffffff);
},
[`${FIS}.isRegularFile0(Ljava/io/FileDescriptor;)Z`]: (t, [, fdObj]) => t.vm.getField(fdObj, "java/io/FileDescriptor", "fd") > 2,
[`${FIS}.isRegularFile0(Ljava/io/FileDescriptor;)Z`]: (t, [, fdObj]) => t.vm.getField(fdObj, "java/io/FileDescriptor", "fd") > 2 && !processPipe(t, t.vm.getField(fdObj, "java/io/FileDescriptor", "fd")),

[`${RAF}.open0(Ljava/lang/String;I)V`]: (t, [self, path, mode]) =>
setFd(t, self, RAF, openFile(t, t.vm.jsString(path), (mode & 2) !== 0, false, false)),
Expand Down
14 changes: 11 additions & 3 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { SnapshotImage } from "./snapshot.ts";
import { hostNatives } from "./hostnatives.ts";
import { ImageBackend, imageFd } from "./image.ts";
import { NodeNet } from "./net-node.ts";
import { processNatives } from "./process-node.ts";
import { PendingThrow } from "./thread.ts";
import { setMaxHeap } from "./runtime.ts";
import type { NetBackend } from "./sockets.ts";
Expand Down Expand Up @@ -351,7 +352,7 @@ function nodeVm(o: NodeRunOptions): JVM {
vfs.mount("/", new NodeBackend("/", true));
// built as dist/node.js, it reads the packaged image next to it
const packaged = !o.jdk && fs.existsSync(path.join(here, "boot.pack"));
const jdk = o.jdk ?? defaultJdk;
const jdk = path.resolve(o.jdk ?? defaultJdk);
if (!packaged && !fs.existsSync(path.join(jdk, "lib", "packages.txt"))) {
throw new Error(`no JDK image at ${jdk}: run \`pnpm jdk\` in a checkout, or keep dist/node.js next to boot.pack and jdk/`);
}
Expand Down Expand Up @@ -397,6 +398,7 @@ function nodeVm(o: NodeRunOptions): JVM {
process.stdin.resume();
}),
});
vm.registerNatives(processNatives(fileURLToPath(import.meta.url), jdk, packaged, o.input === undefined));
bridges.set(vm, bridge);
// A runaway must not take the machine down; per-site caps catch most.
const maxRss = (o.maxRssMb ?? 2048) * 2 ** 20;
Expand Down Expand Up @@ -476,6 +478,8 @@ function heapSize(s: string): number {
export function main(argv: string[]) {
const o: NodeRunOptions = { classpath: ["."], props: {} };
let i = 0;
const javaChild = argv[0] === "--java-child";
if (javaChild) i++;
for (; i < argv.length; i++) {
const a = argv[i];
if (a === "-cp" || a === "-classpath" || a === "--class-path") o.classpath = argv[++i].split(":");
Expand All @@ -498,10 +502,14 @@ export function main(argv: string[]) {
}
setMaxHeap(Math.ceil(n / 2 ** 21) * 2 ** 21);
}
else if (/^(-X|--add-|-verbose|-server|-client)/.test(a)) continue;
else if (a === "--jdk") o.jdk = argv[++i];
else if (a === "--max-rss") o.maxRssMb = Number(argv[++i]);
else if (a === "-jar") { o.jar = argv[++i]; i++; break; }
else if (/^(-X|--add-|-verbose|-server|-client)/.test(a)) continue;
else if (javaChild && a.startsWith("-")) {
console.error(`Unrecognized option: ${a}\nError: Could not create the Java Virtual Machine.\nError: A fatal exception has occurred. Program will exit.`);
process.exit(1);
}
else if (a === "--max-rss") o.maxRssMb = Number(argv[++i]);
else { o.mainClass = a; i++; break; }
}
o.args = argv.slice(i);
Expand Down
Loading
Loading