diff --git a/docs/node.md b/docs/node.md index ec8a495..7941908 100644 --- a/docs/node.md +++ b/docs/node.md @@ -49,6 +49,8 @@ resolves to the exit code. Its options: - `natives`: Java `native` methods in JS, as in the browser ([browser.md](browser.md)) - `assertions` (the flags, for example `["-ea"]`), `enablePreview`, `localeData` - `hostName` (what `InetAddress.getLocalHost()` reports), `maxRssMb`, `jdk`, `debug` +- `maxDirectMemory` (`-XX:MaxDirectMemorySize`, in bytes), `onOutOfMemoryError` (`-XX:OnOutOfMemoryError` commands), + `vmOutputToStderr` (`-XX:+DisplayVMOutputToStderr`) - `fetch`: sockets stay inside the VM, and `http:` requests go to this `fetch`, as in the browser - `snapshotDir`: where boot snapshots are kept, or `null` for none (see below) diff --git a/docs/support.md b/docs/support.md index 21fc4d4..f1d95d4 100644 --- a/docs/support.md +++ b/docs/support.md @@ -29,7 +29,7 @@ java-web targets headless Java: console apps, libraries and javac. host's inode numbers and link counts, and the modes `open` and `mkdir` ask for, less the process's umask. In-memory files get synthetic inode numbers, a link count of 1 and default modes. -- `-XX:OnOutOfMemoryError` runs no command, and `StackWalker` +- `-XX:OnOutOfMemoryError` runs its commands under Node only, and `StackWalker` has no live locals or operands. A virtual thread that yields while it holds a monitor stays pinned to its carrier. - `ThreadMXBean` describes threads and finds deadlocks, but has no CPU times @@ -37,8 +37,10 @@ java-web targets headless Java: console apps, libraries and javac. find a frame's locked monitors from its synchronized blocks as compilers emit them, and list a monitor taken any other way with stack depth -1. - `WatchService` reports files the program itself, or another thread in the - same VM, creates, changes or deletes. It does not see changes made outside - the VM, including by child processes the program starts. + same VM, creates, changes or deletes. Under Node it also reports what other + processes do to a watched host directory, with Node's `fs.watch` (so a short-lived + entry can show as a delete without its create). Elsewhere it does not see + changes made outside the VM. - Not supported: GUI apps (AWT and Swing windows), `ConvolveOp`, child processes in the browser, and native libraries other than JS natives. diff --git a/src/fs.ts b/src/fs.ts index ee50a89..d621d0e 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -16,8 +16,13 @@ export interface Stat { // the modification time in nanoseconds, when it was set with more precision // than mtime keeps mtimeNs?: bigint; + // the access time, when the backend keeps one (mtime otherwise), and in nanoseconds + atime?: number; + atimeNs?: bigint; // permission and special bits, when the backend can report them mode?: number; + // a socket, FIFO or device on the host, which cannot be opened here + special?: boolean; // a character device; it streams and is never read whole device?: boolean; // lstat of a symbolic link: the length of its target, which is also its size @@ -60,8 +65,8 @@ export interface Backend { rename?(from: string, to: string): boolean; // the file system holding path: block size and block counts, as statvfs gives them space?(path: string): Space | null; - // sets the modification time, in milliseconds since the epoch - setMtime?(path: string, ms: number, noFollow?: boolean): boolean; + // sets the modification time, and the access time when given, in milliseconds since the epoch + setMtime?(path: string, ms: number, noFollow?: boolean, atimeMs?: number): boolean; // whether the host refuses to write the file, or to create it when it is missing denied?(path: string): boolean; // sets the permission bits to f(current bits) @@ -76,6 +81,10 @@ export interface Backend { link?(existing: string, path: string): string | undefined; // the path with every link resolved, where the missing tail stays as written realpath?(path: string): string | null; + // reports changes others make to a directory's entries (name "" is the directory itself); returns what stops it + watch?(path: string, onEvent: (name: string, event: "create" | "modify" | "delete") => void): (() => void) | null; + // changes whenever the entry's content or times do, or is "" when it is missing + signature?(path: string): string; } export interface WarmTrace { @@ -104,7 +113,7 @@ export function normalize(path: string): string { export class VFS { // path -> the time a VM set, and the mtime the backend reported right after - private setTimes = new Map(); + private setTimes = new Map(); private mounts: { prefix: string; backend: Backend }[] = []; private hidden: string[] = []; private warmTrace: WarmTrace | null = null; @@ -229,6 +238,51 @@ export class VFS { private changedNormalized(path: string, event: "create" | "modify" | "delete") { for (const watcher of this.watchers) watcher(path, event); this.warmChanged?.add(path); + if (this.hostWatches.size === 0) return; + const watch = this.hostWatches.get(parentOf(path)); + if (watch) watch.own.set(path.slice(path.lastIndexOf("/") + 1), this.signature(path)); + } + + // Host directories being watched: the entries as the VM last left them, so + // that only changes others make reach watchers a second time. + private hostWatches = new Map void; refs: number; own: Map }>(); + + private signature(path: string): string { + const r = this.resolve(path); + return (r && r[0].signature?.(r[1])) ?? ""; + } + + // Starts reporting what others do to a directory on a host backend, as + // inotify does; the VM's own changes already reach watchers. Returns what stops it. + watchDirectory(path: string): () => void { + const dir = normalize(path); + let watch = this.hostWatches.get(dir); + if (!watch) { + const r = this.resolve(dir); + const stop = r?.[0].watch?.(r[1], (name, event) => this.hostChanged(dir, name, event)); + if (!stop) return () => {}; + watch = { stop, refs: 0, own: new Map() }; + this.hostWatches.set(dir, watch); + } + const w = watch; + w.refs++; + let done = false; + return () => { + if (done) return; + done = true; + if (--w.refs === 0) { + w.stop(); + if (this.hostWatches.get(dir) === w) this.hostWatches.delete(dir); + } + }; + } + + private hostChanged(dir: string, name: string, event: "create" | "modify" | "delete") { + const watch = this.hostWatches.get(dir); + if (!watch) return; + const path = name === "" ? dir : (dir === "/" ? "" : dir) + "/" + name; + if (name !== "" && watch.own.get(name) === this.signature(path)) return; + for (const watcher of this.watchers) watcher(path, event); } resolvePath(path: string): string { @@ -289,7 +343,10 @@ export class VFS { const s = r ? r[0].stat(r[1]) : null; if (s) { const set = this.setTimes.get(n); - if (set?.ms === s.mtime) return { ...s, mtimeNs: set.ns }; + if (set?.ms === s.mtime) { + const atime = set.atime && set.atime.ms === s.atime ? { atimeNs: set.atime.ns } : {}; + return { ...s, mtimeNs: set.ns, ...atime }; + } if (set) this.setTimes.delete(n); return s; } @@ -474,16 +531,18 @@ export class VFS { // ns, when given, is the exact time asked for. Hosts keep times as floating // point seconds or milliseconds, which lose nanoseconds, so stat reports ns // for as long as the file keeps the time this set. - setMtime(path: string, ms: number, ns?: bigint, noFollow = false): boolean { + setMtime(path: string, ms: number, ns?: bigint, noFollow = false, atime?: { ms: number; ns: bigint }): boolean { const r = this.resolve(path); - if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms, noFollow)) return false; + if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms, noFollow, atime?.ms)) return false; this.changed(path, "modify"); if (noFollow) return true; const n = normalize(path); const st = ns === undefined ? null : r[0].stat(r[1]); // only a time the host kept, give or take its rounding; a host can clamp it - if (st && Math.abs(st.mtime - ms) < 1) this.setTimes.set(n, { ms: st.mtime, ns: ns! }); - else this.setTimes.delete(n); + if (st && Math.abs(st.mtime - ms) < 1) { + const kept = atime && st.atime !== undefined && Math.abs(st.atime - atime.ms) < 1; + this.setTimes.set(n, { ms: st.mtime, ns: ns!, ...(kept ? { atime: { ms: st.atime!, ns: atime.ns } } : {}) }); + } else this.setTimes.delete(n); return true; } diff --git a/src/jvm.ts b/src/jvm.ts index d6ccc17..d097c0b 100644 --- a/src/jvm.ts +++ b/src/jvm.ts @@ -57,6 +57,8 @@ export interface JvmOptions { loadNativeLibrary?: (path: string) => Promise; // told when Java closes a file it wrote fileClosed?: (path: string) => void; + // runs before the first OutOfMemoryError the VM throws is created + onOutOfMemory?: (message: string | null) => void; // log VM internals to the host console debug?: boolean; assertions?: Assertions; @@ -837,6 +839,7 @@ export class JVM { *newThrowable(t: JThread, name: string, msg: string | null, cause: JObject | null = null, ctor?: { desc: string; args: any[] }): NativeGen { + if (name === "java/lang/OutOfMemoryError") this.reportOutOfMemory(msg); const c = this.loadBootOrFail(name); if (c.state !== INITIALIZED) yield* this.initGen(t, c); const o = this.newObject(c); @@ -852,6 +855,13 @@ export class JVM { return o; } + private oomReported = false; + private reportOutOfMemory(msg: string | null) { + if (this.oomReported || !this.opts.onOutOfMemory) return; + this.oomReported = true; + this.opts.onOutOfMemory(msg); + } + // native: the native method throwing it, shown in its stack trace throwNew(t: JThread, name: string, msg: string | null, ctor?: { desc: string; args: any[] }, native: JMethod | null = null) { const vm = this; diff --git a/src/natives/io.ts b/src/natives/io.ts index 66f9db6..ea08329 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -412,9 +412,9 @@ export function syncPath(t: JThread, abs: string) { } // Pending writes go out first, so closing the file later does not change the time again. -export function setMtime(t: JThread, abs: string, ms: number, ns?: bigint, noFollow = false): boolean { +export function setMtime(t: JThread, abs: string, ms: number, ns?: bigint, noFollow = false, atime?: { ms: number; ns: bigint }): boolean { syncPath(t, abs); - return t.vm.fs.setMtime(abs, ms, ns, noFollow); + return t.vm.fs.setMtime(abs, ms, ns, noFollow, atime); } // write(2) on a descriptor opened read-only fails with EBADF. diff --git a/src/natives/nio.ts b/src/natives/nio.ts index 792f076..7bc9ab9 100644 --- a/src/natives/nio.ts +++ b/src/natives/nio.ts @@ -10,12 +10,12 @@ import { closeFd, DirectoryFile, newFd, fileInode, fileOf, flush, flushOrThrow, import type { OpenFile } from "./io.ts"; import type { Natives } from "./util.ts"; -export const EPERM = 1, ENOENT = 2, EINTR = 4, EIO = 5, EBADF = 9, ENOMEM = 12, EACCES = 13, +export const EPERM = 1, ENOENT = 2, EINTR = 4, EIO = 5, ENXIO = 6, EBADF = 9, ENOMEM = 12, EACCES = 13, EEXIST = 17, EXDEV = 18, ENOTDIR = 20, EISDIR = 21, EINVAL = 22, ENOSPC = 28, EROFS = 30, ENOTEMPTY = 39, ENOSYS = 38, ENAMETOOLONG = 36, ELOOP = 40, ENOTSUP = 95, ESTALE = 116, EDQUOT = 122; const MESSAGES: Record = { - [EPERM]: "Operation not permitted", [EINTR]: "Interrupted system call", [EIO]: "Input/output error", + [EPERM]: "Operation not permitted", [ENXIO]: "No such device or address", [EINTR]: "Interrupted system call", [EIO]: "Input/output error", [ENOMEM]: "Cannot allocate memory", [ENOSPC]: "No space left on device", [ELOOP]: "Too many levels of symbolic links", [ENOTSUP]: "Operation not supported", [ESTALE]: "Stale file handle", [ENAMETOOLONG]: "File name too long", [EDQUOT]: "Disk quota exceeded", @@ -103,10 +103,12 @@ function renameAbs(t: JThread, from: string, to: string) { } function openAbs(t: JThread, abs: string, flags: number, mode: number): number { + // before any stat that follows the link, which would touch its access time + if (flags & O_NOFOLLOW && !(flags & O_CREAT && flags & O_EXCL) && t.vm.fs.lstat(abs)?.link !== undefined) unixError(ELOOP); const st = t.vm.fs.stat(abs); if ((flags & O_CREAT) && (flags & O_EXCL) && (st || t.vm.fs.lstat(abs))) unixError(EEXIST); + if (st?.special) unixError(ENXIO); if (!st && !(flags & O_CREAT)) unixError(ENOENT); - if (flags & O_NOFOLLOW && t.vm.fs.lstat(abs)?.link !== undefined) unixError(ELOOP); if (flags & O_DIRECTORY && st && !st.dir) unixError(ENOTDIR); const write = (flags & O_ACCMODE) !== 0; if (st?.dir && write) unixError(EISDIR); @@ -162,16 +164,18 @@ function fillAttrs(t: JThread, attrs: JObject, p: string, follow = true): number return 0; } +// seconds and nanoseconds of a time kept as milliseconds, or exactly as nanoseconds +function splitTime(ms: number, ns?: bigint): [bigint, bigint] { + if (ns === undefined) return [BigInt(Math.floor(ms / 1000)), BigInt(Math.floor((ms % 1000) * 1e6))]; + const sec = ns / 1_000_000_000n, nsec = ns % 1_000_000_000n; + // before 1970: whole seconds round down, nanoseconds stay positive + return nsec < 0n ? [sec - 1n, nsec + 1_000_000_000n] : [sec, nsec]; +} + function fillStat(t: JThread, attrs: JObject, st: Stat, writable: boolean, ino: number, size = st.size, nlink = 1) { const vm = t.vm, A = "sun/nio/fs/UnixFileAttributes"; const mode = st.device ? S_IFCHR | 0o666 : (st.link !== undefined ? S_IFLNK : st.dir ? S_IFDIR : S_IFREG) | (st.mode ?? (st.dir ? (writable ? 0o755 : 0o555) : (writable ? 0o644 : 0o444))); - let sec = BigInt(Math.floor(st.mtime / 1000)), nsec = BigInt(Math.floor((st.mtime % 1000) * 1e6)); - if (st.mtimeNs !== undefined) { - sec = st.mtimeNs / 1_000_000_000n; - nsec = st.mtimeNs % 1_000_000_000n; - // before 1970: whole seconds round down, nanoseconds stay positive - if (nsec < 0n) [sec, nsec] = [sec - 1n, nsec + 1_000_000_000n]; - } + const [sec, nsec] = splitTime(st.mtime, st.mtimeNs); vm.setField(attrs, A, "st_mode", mode); vm.setField(attrs, A, "st_ino", BigInt(ino)); vm.setField(attrs, A, "st_dev", 1n); @@ -179,8 +183,11 @@ function fillStat(t: JThread, attrs: JObject, st: Stat, writable: boolean, ino: vm.setField(attrs, A, "st_uid", 1000); vm.setField(attrs, A, "st_gid", 1000); vm.setField(attrs, A, "st_size", BigInt(size)); - for (const k of ["st_atime_sec", "st_mtime_sec", "st_ctime_sec"]) vm.setField(attrs, A, k, sec); - for (const k of ["st_atime_nsec", "st_mtime_nsec", "st_ctime_nsec"]) vm.setField(attrs, A, k, nsec); + for (const k of ["st_mtime_sec", "st_ctime_sec"]) vm.setField(attrs, A, k, sec); + for (const k of ["st_mtime_nsec", "st_ctime_nsec"]) vm.setField(attrs, A, k, nsec); + const [asec, ansec] = splitTime(st.atime ?? st.mtime, st.atimeNs); + vm.setField(attrs, A, "st_atime_sec", asec); + vm.setField(attrs, A, "st_atime_nsec", ansec); } // open mount table handles: whether the one entry has been read @@ -310,15 +317,15 @@ export const natives: Natives = { [`${N}chown0(JII)V`]: () => undefined, [`${N}lchown0(JII)V`]: () => undefined, [`${N}fchown0(III)V`]: () => undefined, - // times in nanoseconds; only the modification time is kept - [`${N}utimensat0(IJJJI)V`]: (t, [dfd, p, , mtime, flags]) => { + // times in nanoseconds; in-memory files keep only the modification time + [`${N}utimensat0(IJJJI)V`]: (t, [dfd, p, atime, mtime, flags]) => { const abs = pathAt(t, dfd, p); if (!t.vm.fs.lstat(abs)) unixError(ENOENT); - if (!setMtime(t, abs, Number(mtime) / 1e6, BigInt(mtime), (flags & AT_SYMLINK_NOFOLLOW) !== 0)) unixError(EROFS); + if (!setMtime(t, abs, Number(mtime) / 1e6, BigInt(mtime), (flags & AT_SYMLINK_NOFOLLOW) !== 0, { ms: Number(atime) / 1e6, ns: BigInt(atime) })) unixError(EROFS); }, - [`${N}futimens0(IJJ)V`]: (t, [fd, , mtime]) => { + [`${N}futimens0(IJJ)V`]: (t, [fd, atime, mtime]) => { const f = fileOf(t, fd); - if (!setMtime(t, f.path, Number(mtime) / 1e6, BigInt(mtime))) unixError(EROFS); + if (!setMtime(t, f.path, Number(mtime) / 1e6, BigInt(mtime), false, { ms: Number(atime) / 1e6, ns: BigInt(atime) })) unixError(EROFS); }, [`${N}opendir0(J)J`]: (t, [p]) => { const abs = path(t, p); diff --git a/src/natives/sockets.ts b/src/natives/sockets.ts index beb33a7..193d507 100644 --- a/src/natives/sockets.ts +++ b/src/natives/sockets.ts @@ -18,7 +18,7 @@ const FD = "java/io/FileDescriptor"; const IOS_EOF = -1, IOS_UNAVAILABLE = -2, IOS_INTERRUPTED = -3; const EEXIST = 17, EBADF = 9, EPROTO = 71, ENOTDIR = 20, EFAULT = 14, ENAMETOOLONG = 36; -type Inotify = { sock: Sock; watches: Map; paths: Map; nextWd: number; overflowed: boolean; remove: () => void }; +type Inotify = { sock: Sock; watches: Map void }>; paths: Map; nextWd: number; overflowed: boolean; remove: () => void }; const IN_MODIFY = 0x2, IN_ATTRIB = 0x4, IN_MOVED_FROM = 0x40, IN_MOVED_TO = 0x80; const IN_CREATE = 0x100, IN_DELETE = 0x200, IN_Q_OVERFLOW = 0x4000; const IN_IGNORED = 0x8000; @@ -47,6 +47,7 @@ function initInotify(t: JThread): number { view.setInt32(0, wd, true); view.setUint32(4, IN_IGNORED, true); sock.push(bytes); + watch.stop(); state.watches.delete(wd); state.paths.delete(path); continue; @@ -78,13 +79,18 @@ function initInotify(t: JThread): number { }); const fd = register(t, sock); all.set(fd, state); - t.vm.exited.then(() => state.remove()); + t.vm.exited.then(() => { + for (const watch of state.watches.values()) watch.stop(); + state.remove(); + }); return fd; } // Stops an inotify descriptor's watches before the descriptor closes. export function closeInotify(t: JThread, fd: number) { - inotify(t, fd)?.remove(); + const state = inotify(t, fd); + if (state) for (const watch of state.watches.values()) watch.stop(); + state?.remove(); (t.vm.state.get("inotify") as Map | undefined)?.delete(fd); } @@ -165,6 +171,8 @@ export function socketError(errno: number, msg?: string): PendingThrow { } const ioError = (errno: number) => new PendingThrow("java/io/IOException", strerror(errno)); +// what the native methods the JDK declares with `throws UnixException` throw +const unixError = (errno: number) => new PendingThrow("sun/nio/fs/UnixException", null, { desc: "(I)V", args: [errno] }); const connectionReset = () => new PendingThrow("sun/net/ConnectionResetException", "Connection reset"); // Reads and discards up to max wake-up bytes; an event counter counts as one. @@ -238,9 +246,9 @@ function* pollPair(t: JThread, a: Sock, b: Sock): NativeGen { function nativePath(t: JThread, address: bigint): string { const bytes = t.vm.mem.u8; const start = Number(address); - if (start <= 0 || start >= bytes.length) throw ioError(EFAULT); + if (start <= 0 || start >= bytes.length) throw unixError(EFAULT); const len = bytes.subarray(start, start + 4096).indexOf(0); - if (len < 0) throw ioError(ENAMETOOLONG); + if (len < 0) throw unixError(ENAMETOOLONG); return resolvePath(t, new TextDecoder().decode(bytes.subarray(start, start + len))); } @@ -544,31 +552,33 @@ export const natives: Natives = { "sun/nio/fs/LinuxWatchService.inotifyInit()I": (t) => initInotify(t), "sun/nio/fs/LinuxWatchService.inotifyAddWatch(IJI)I": (t, [fd, address, mask]) => { const state = inotify(t, fd); - if (!state || !socksOf(t.vm.state).has(fd)) throw ioError(EBADF); + if (!state || !socksOf(t.vm.state).has(fd)) throw unixError(EBADF); const path = nativePath(t, address); const st = t.vm.fs.stat(path); - if (!st) throw ioError(ENOENT); - if (!st.dir) throw ioError(ENOTDIR); + if (!st) throw unixError(ENOENT); + if (!st.dir) throw unixError(ENOTDIR); let wd = state.paths.get(path); if (wd === undefined) { wd = state.nextWd++; state.paths.set(path, wd); } - state.watches.set(wd, { path, mask: mask & WATCH_EVENTS }); + const stop = state.watches.get(wd)?.stop ?? t.vm.fs.watchDirectory(path); + state.watches.set(wd, { path, mask: mask & WATCH_EVENTS, stop }); return wd; }, "sun/nio/fs/LinuxWatchService.inotifyRmWatch(II)V": (t, [fd, wd]) => { const state = inotify(t, fd); - if (!state) throw ioError(EBADF); + if (!state) throw unixError(EBADF); const watch = state.watches.get(wd); if (watch) { + watch.stop(); state.watches.delete(wd); state.paths.delete(watch.path); } }, "sun/nio/fs/LinuxWatchService.configureBlocking(IZ)V": (t, [fd, blocking]) => { const s = socksOf(t.vm.state).get(fd); - if (!s) throw ioError(EBADF); + if (!s) throw unixError(EBADF); s.blocking = blocking === 1; }, "sun/nio/fs/LinuxWatchService.socketpair([I)V": (t, [sv]) => { @@ -579,12 +589,21 @@ export const natives: Natives = { }, "sun/nio/fs/LinuxWatchService.poll(II)I": function* (t, [fd1, fd2]): NativeGen { const a = socksOf(t.vm.state).get(fd1), b = socksOf(t.vm.state).get(fd2); - if (!a || !b) throw ioError(EBADF); + if (!a || !b) throw unixError(EBADF); return yield* pollPair(t, a, b); }, "sun/nio/fs/UnixNativeDispatcher.read0(IJI)I": function* (t, [fd, address, len]): NativeGen { const s = socksOf(t.vm.state).get(fd); if (!s) throw new PendingThrow("sun/nio/fs/UnixException", null, { desc: "(I)V", args: [EBADF] }); + // an inotify read returns whole events only + if (inotify(t, fd)) { + let whole = 0; + for (const event of s.rx) { + if (whole + event.length > len) break; + whole += event.length; + } + if (whole > 0) len = whole; + } const n = yield* recv(t, s, len, (count) => s.take(t.vm.mem.u8, Number(address), count)); if (n === IOS_UNAVAILABLE) throw new PendingThrow("sun/nio/fs/UnixException", null, { desc: "(I)V", args: [11] }); return n; diff --git a/src/node.ts b/src/node.ts index 1e9d9a6..8e10bc9 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,6 +1,7 @@ // Node entry: java-web [-cp path] [-Dk=v] [--debug] Main [args] | -jar app.jar [args] 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 * as path from "node:path"; @@ -16,7 +17,7 @@ import type { SnapshotImage, WarmSnapshotImage } 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 { ENV_CHANNEL, javaChild, processNatives } from "./process-node.ts"; import { PendingThrow } from "./thread.ts"; import { heapSize } from "./heapsize.ts"; import { clearAllocationCheck, hasMaxHeap, MAX_HEAP, resetAllocationCheck, setAllocationCheck, setMaxHeap } from "./runtime.ts"; @@ -83,7 +84,8 @@ export class NodeBackend implements Backend { let st: Stat | null = null; try { const s = fs.statSync(this.host(p)); - st = { dir: s.isDirectory(), size: s.size, mtime: s.mtimeMs, mode: s.mode & 0o7777, ino: s.ino, nlink: s.nlink }; + st = { dir: s.isDirectory(), size: s.size, mtime: s.mtimeMs, atime: s.atimeMs, mode: s.mode & 0o7777, ino: s.ino, nlink: s.nlink }; + if (!s.isDirectory() && !s.isFile()) st.special = true; } catch { st = null; } @@ -94,7 +96,7 @@ export class NodeBackend implements Backend { try { const s = fs.lstatSync(this.host(p)); if (!s.isSymbolicLink()) return null; - return { dir: false, size: s.size, mtime: s.mtimeMs, mode: s.mode & 0o7777, link: s.size, ino: s.ino, nlink: s.nlink }; + return { dir: false, size: s.size, mtime: s.mtimeMs, atime: s.atimeMs, mode: s.mode & 0o7777, link: s.size, ino: s.ino, nlink: s.nlink }; } catch { return null; } @@ -139,7 +141,19 @@ export class NodeBackend implements Backend { try { const s = fs.statSync(this.host(p)); if (!s.isFile() || s.size > MAX_FILE) return null; - return new Uint8Array(fs.readFileSync(this.host(p))); + // opening a file does not read it on the host, so it must not touch the access time + let fd: number; + try { + fd = fs.openSync(this.host(p), fs.constants.O_RDONLY | (fs.constants.O_NOATIME ?? 0)); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== "EPERM") throw e; + fd = fs.openSync(this.host(p), fs.constants.O_RDONLY); + } + try { + return new Uint8Array(fs.readFileSync(fd)); + } finally { + fs.closeSync(fd); + } } catch { return null; } @@ -232,10 +246,40 @@ export class NodeBackend implements Backend { return null; } } - setMtime(p: string, ms: number, noFollow = false) { + watch(p: string, onEvent: (name: string, event: "create" | "modify" | "delete") => void) { + const host = this.host(p); + try { + const exists = (file: string) => { + try { + return !!fs.lstatSync(file, { throwIfNoEntry: false }); + } catch { + return false; + } + }; + const watcher = fs.watch(host, { persistent: false }, (type, name) => { + if (name === null) return; + const child = exists(path.join(host, name)); + // the directory's own events carry its name + if (name === path.basename(host) && !child) return exists(host) ? undefined : onEvent("", "delete"); + if (type === "change") return onEvent(name, "modify"); + onEvent(name, child ? "create" : "delete"); + }); + watcher.on("error", () => {}); + return () => watcher.close(); + } catch { + return null; + } + } + signature(p: string) { + const s = fs.lstatSync(this.host(p), { bigint: true, throwIfNoEntry: false }); + return s ? `${s.ino}:${s.size}:${s.mtimeNs}` : ""; + } + setMtime(p: string, ms: number, noFollow = false, atimeMs?: number) { try { - if (noFollow) fs.lutimesSync(this.host(p), fs.lstatSync(this.host(p)).atime, ms / 1000); - else fs.utimesSync(this.host(p), fs.statSync(this.host(p)).atime, ms / 1000); + const host = this.host(p); + const atime = atimeMs === undefined ? (noFollow ? fs.lstatSync(host) : fs.statSync(host)).atimeMs : atimeMs; + if (noFollow) fs.lutimesSync(host, atime / 1000, ms / 1000); + else fs.utimesSync(host, atime / 1000, ms / 1000); return true; } catch { return false; @@ -292,6 +336,12 @@ export interface NodeRunOptions { // the process exits when its resident memory goes past this maxRssMb?: number; enablePreview?: boolean; + // -XX:OnOutOfMemoryError commands, run through /bin/sh on the first OutOfMemoryError the VM throws + onOutOfMemoryError?: string[]; + // -XX:+DisplayVMOutputToStderr: the VM's own messages go to stderr, not stdout + vmOutputToStderr?: boolean; + // -XX:MaxDirectMemorySize in bytes + maxDirectMemory?: number; // what InetAddress.getLocalHost() reports instead of the host's name hostName?: string; // what System.getenv() returns instead of the host's environment @@ -347,6 +397,23 @@ function onOutError(this: NodeJS.WriteStream) { broken.add(this); } +// What HotSpot does on the first OutOfMemoryError: it prints the commands and +// runs each through /bin/sh, with %p the process id, before throwing. +function runOnOutOfMemory(commands: string[], message: string | null, out: ((b: Uint8Array) => void) | undefined, fd: number, java: string[]) { + const say = (line: string) => out ? out(new TextEncoder().encode(line + "\n")) : fs.writeSync(fd, line + "\n"); + say(`#\n# java.lang.OutOfMemoryError: ${message ?? "null"}`); + for (const raw of commands) { + say(`# -XX:OnOutOfMemoryError="${raw}"`); + for (const part of raw.split(";")) { + const cmd = part.replaceAll("%p", String(process.pid)); + say(`# Executing /bin/sh -c "${cmd}"...`); + // java.home is not a host path, so a command that starts java-web's java starts it directly + const run = cmd.replace(/^\s*(\/jdk\/bin\/java|java)(?=\s|$)/, java.map((w) => `'${w.replaceAll("'", "'\\''")}'`).join(" ")); + spawnSync("/bin/sh", ["-c", run], { stdio: "inherit" }); + } + } +} + function defaultSnapshotDir(): string | null { const env = process.env.JAVA_WEB_SNAPSHOT_DIR; if (env !== undefined) return env || null; @@ -479,7 +546,11 @@ function nodeVm(o: NodeRunOptions): JVM { javaHome: "/jdk", localeData: o.localeData, classpath, - props: { "java.io.tmpdir": os.tmpdir(), "user.home": os.homedir(), "user.name": os.userInfo().username, ...o.props }, + props: { + "java.io.tmpdir": os.tmpdir(), "user.home": os.homedir(), "user.name": os.userInfo().username, ...o.props, + // only the option sets it, as in HotSpot + "sun.nio.MaxDirectMemorySize": String(o.maxDirectMemory ?? -1), + }, cwd: toVfs(process.cwd()), debug: o.debug, assertions: o.assertions && parseAssertions(o.assertions), @@ -505,6 +576,7 @@ function nodeVm(o: NodeRunOptions): JVM { process.stdin.resume(); }), afterBoot: resetAllocationCheck, + onOutOfMemory: o.onOutOfMemoryError && ((message) => runOnOutOfMemory(o.onOutOfMemoryError!, message, o.vmOutputToStderr ? o.stderr : o.stdout, o.vmOutputToStderr ? 2 : 1, javaChild(fileURLToPath(import.meta.url), jdk, packaged))), }); vm.registerNatives(processNatives(fileURLToPath(import.meta.url), jdk, packaged, o.input === undefined)); bridges.set(vm, bridge); @@ -576,12 +648,33 @@ export function moduleFlag(o: NodeRunOptions, arg: string, next: string | undefi return true; } +// java -version: the launcher's three lines, built from the VM's properties +async function printVersion(o: NodeRunOptions) { + const vm = await startLibrary(o); + const System = await libraryOf(vm).java.lang.System; + const [version, date, runtimeName, vendor, runtime, vmName, vmVersion, vmInfo] = await Promise.all( + ["java.version", "java.version.date", "java.runtime.name", "java.vendor.version", "java.runtime.version", "java.vm.name", "java.vm.version", "java.vm.info"] + .map((k) => System.getProperty(k))); + const extra = vendor ? `${vendor} ` : ""; + process.stderr.write(`openjdk version "${version}" ${date}${/-LTS$/.test(runtime) ? " LTS" : ""}\n` + + `${runtimeName} ${extra}(build ${runtime})\n${vmName} ${extra}(build ${vmVersion}, ${vmInfo})\n`); + vm.halt(0); +} + // the java-web command, which dist/cli.js runs export function main(argv: string[]) { const o: NodeRunOptions = { classpath: ["."], props: {} }; let i = 0; + let version = false; const javaChild = argv[0] === "--java-child"; - if (javaChild) i++; + if (javaChild) { + i++; + const sent = process.env[ENV_CHANNEL]; + if (sent !== undefined) { + o.env = JSON.parse(sent); + delete process.env[ENV_CHANNEL]; + } + } for (; i < argv.length; i++) { const a = argv[i]; if (a === "-cp" || a === "-classpath" || a === "--class-path") o.classpath = argv[++i].split(":"); @@ -594,6 +687,7 @@ export function main(argv: string[]) { else if (isAssertionFlag(a)) (o.assertions ??= []).push(a); else if (a === "--enable-preview") o.enablePreview = true; else if (a === "--locale-data") o.localeData = true; + else if (a === "-version") version = true; else if (a.startsWith("-Xmx")) { // x64 HotSpot refuses to start below 2 MB and rounds up to 2 MB steps const n = /^-Xmx\d+[kmg]?$/i.test(a) ? heapSize(a.slice(4)) : 0; @@ -604,6 +698,17 @@ export function main(argv: string[]) { } setMaxHeap(Math.ceil(n / 2 ** 21) * 2 ** 21); } + else if (a === "-XX:+DisplayVMOutputToStderr") o.vmOutputToStderr = true; + else if (a === "-XX:-DisplayVMOutputToStderr") o.vmOutputToStderr = false; + else if (a.startsWith("-XX:OnOutOfMemoryError=")) (o.onOutOfMemoryError ??= []).push(a.slice(23)); + else if (a.startsWith("-XX:MaxDirectMemorySize=")) { + const n = heapSize(a.slice(24)); + if (!(n >= 0) || !Number.isInteger(n)) { + console.error(`Improperly specified VM option '${a.slice(4)}'\nError: Could not create the Java Virtual Machine.\nError: A fatal exception has occurred. Program will exit.`); + process.exit(1); + } + o.maxDirectMemory = n; + } else if (a === "--jdk") o.jdk = argv[++i]; else if (a === "-jar") { o.jar = argv[++i]; i++; break; } else if (/^(-X|--add-|-verbose|-server|-client)/.test(a)) continue; @@ -615,6 +720,13 @@ export function main(argv: string[]) { else { o.mainClass = a; i++; break; } } o.args = argv.slice(i); + if (version) { + printVersion(o).then(() => process.exit(0), (e) => { + console.error(`java-web: ${(e as Error).message}`); + process.exit(1); + }); + return; + } if (!o.mainClass && !o.jar) { console.error("usage: java-web [-cp path] [-Dkey=value] Main [args] | -jar app.jar [args]"); process.exit(2); diff --git a/src/process-node.ts b/src/process-node.ts index 3a17bcd..862e778 100644 --- a/src/process-node.ts +++ b/src/process-node.ts @@ -12,6 +12,13 @@ import type { OpenFile, ProcessPipe } from "./natives/io.ts"; import type { Natives } from "./natives/util.ts"; import { u8 } from "./natives/util.ts"; +// the variable that carries a java-web child's environment when Node cannot +export const ENV_CHANNEL = "JAVA_WEB_ENV"; + +// the command that starts a java-web VM as the `java` of a child process +export const javaChild = (entry: string, jdk: string, packaged: boolean) => + [process.execPath, entry, "--java-child", ...(packaged ? [] : ["--jdk", jdk])]; + const drained = (output: Writable) => output.writableNeedDrain ? new Promise((resolve) => { output.once("drain", resolve); output.once("close", resolve); }) : undefined; @@ -189,13 +196,17 @@ export function processNatives(entry: string, jdk: string, packaged: boolean, ho const testJdk = t.vm.props.get("test.jdk"); const java = command === "java" || command === "/jdk/bin/java" || command === `${jdk}/bin/java` || !!(testJdk && command === `${testJdk}/bin/java`); if (java) { - command = process.execPath; - args = [entry, "--java-child", ...(packaged ? [] : ["--jdk", jdk]), ...args]; + [command, ...args] = [...javaChild(entry, jdk, packaged), ...args]; } - const env = envBlock === null ? t.vm.opts.env : Object.fromEntries(block(envBlock).map((s) => { - const i = s.indexOf("=", 1); + let env = envBlock === null ? t.vm.opts.env : Object.fromEntries(block(envBlock).map((s) => { + const i = s.indexOf("="); return i < 0 ? [s, ""] : [s.slice(0, i), s.slice(i + 1)]; })); + // a host process cannot have a variable with an empty name; the java-web child reads it from here + if (java && env && "" in env) { + const { "": _, ...rest } = env; + env = { ...rest, [ENV_CHANNEL]: JSON.stringify(env) }; + } const descriptors = (fds as JArray).a as Int32Array; const opened: number[] = []; const stdio: (number | "pipe" | Socket)[] = []; diff --git a/src/runtime.ts b/src/runtime.ts index 64de9f3..8bbdad1 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -508,6 +508,7 @@ function store(bytes: number): [ArrayBuffer, number] | null { } export function newArrayData(elem: string, n: number): ArrayData { + if (n > 0x7ffffffd) throw new PendingThrow("java/lang/OutOfMemoryError", "Requested array size exceeds VM limit"); const bytes = n * ELEM_SIZE[elem]; if (bytes > MAX_HEAP) throw new PendingThrow("java/lang/OutOfMemoryError", "Java heap space"); const at = elem === "L" ? null : store(bytes); diff --git a/test/java/AccessTimeKept.java b/test/java/AccessTimeKept.java new file mode 100644 index 0000000..f72827f --- /dev/null +++ b/test/java/AccessTimeKept.java @@ -0,0 +1,22 @@ +import java.nio.file.*; +import java.nio.file.attribute.*; + +// attribute calls and opening a file do not read it, so they leave its access time alone +public class AccessTimeKept { + public static void main(String[] args) throws Exception { + Path dir = Files.createTempDirectory("atime"); + Path file = Files.createFile(dir.resolve("foo")); + try { + BasicFileAttributes first = Files.readAttributes(file, BasicFileAttributes.class); + FileTime mod = first.lastModifiedTime(); + Files.setAttribute(file, "basic:lastModifiedTime", FileTime.fromMillis(0L)); + Files.setAttribute(file, "lastModifiedTime", mod); + BasicFileAttributes last = Files.readAttributes(file, BasicFileAttributes.class); + System.out.println(last.lastAccessTime().equals(first.lastAccessTime())); + System.out.println(last.lastModifiedTime().equals(mod)); + } finally { + Files.delete(file); + Files.delete(dir); + } + } +} diff --git a/test/java/ChildJava.java b/test/java/ChildJava.java new file mode 100644 index 0000000..c2b733b --- /dev/null +++ b/test/java/ChildJava.java @@ -0,0 +1,29 @@ +import java.io.File; +import java.util.Map; +import java.util.TreeMap; + +public class ChildJava { + static String run(String[] cmd, String[] env, boolean err) throws Exception { + Process p = Runtime.getRuntime().exec(cmd, env); + byte[] out = (err ? p.getErrorStream() : p.getInputStream()).readAllBytes(); + p.waitFor(); + return new String(out); + } + + public static void main(String[] args) throws Exception { + if (args.length > 0) { + System.out.println(new TreeMap<>(System.getenv())); + return; + } + String java = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + String cp = System.getProperty("java.class.path"); + String version = run(new String[] {java, "-version"}, null, true); + String[] lines = version.split("\n"); + System.out.println(lines[0]); + System.out.println(lines[1]); + System.out.println(lines.length); + // a name that starts with '=' reaches the child as an empty name + String[] env = {"=ExitValue=3", "=C:=\\", "A=b"}; + System.out.print(run(new String[] {java, "-cp", cp, "ChildJava", "child"}, env, false)); + } +} diff --git a/test/java/DirectMemoryLimit.java b/test/java/DirectMemoryLimit.java new file mode 100644 index 0000000..a71d4d4 --- /dev/null +++ b/test/java/DirectMemoryLimit.java @@ -0,0 +1,22 @@ +// java: -XX:MaxDirectMemorySize=3k -Dsun.nio.MaxDirectMemorySize=1m +import java.nio.ByteBuffer; + +public class DirectMemoryLimit { + static void allocate(int n) { + try { + ByteBuffer.allocateDirect(n); + System.out.println("allocated " + n); + } catch (OutOfMemoryError e) { + System.out.println(e.getMessage()); + } + } + + public static void main(String[] args) { + System.out.println(System.getProperty("sun.nio.MaxDirectMemorySize")); + ByteBuffer keep = ByteBuffer.allocateDirect(2048); + allocate(1024); + allocate(1); + allocate(4096); + keep = null; + } +} diff --git a/test/java/OutOfMemoryHook.java b/test/java/OutOfMemoryHook.java new file mode 100644 index 0000000..01a2c78 --- /dev/null +++ b/test/java/OutOfMemoryHook.java @@ -0,0 +1,20 @@ +// java: -XX:OnOutOfMemoryError=echo +public class OutOfMemoryHook { + public static void main(String[] args) { + System.out.println("start"); + System.out.flush(); + for (int i = 0; i < 2; i++) { + try { + Object o = new double[Integer.MAX_VALUE]; + } catch (OutOfMemoryError e) { + System.out.println("caught " + e.getMessage()); + } + } + try { + Object o = new byte[Integer.MAX_VALUE - 1][]; + } catch (OutOfMemoryError e) { + System.out.println("caught " + e.getMessage()); + } + Object o = new int[Integer.MAX_VALUE]; + } +} diff --git a/test/java/SymlinkTimes.java b/test/java/SymlinkTimes.java new file mode 100644 index 0000000..4af12a5 --- /dev/null +++ b/test/java/SymlinkTimes.java @@ -0,0 +1,55 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +public class SymlinkTimes { + static BasicFileAttributes read(Path p, LinkOption... o) throws IOException { + return Files.readAttributes(p, BasicFileAttributes.class, o); + } + + static void show(String label, BasicFileAttributes a) { + System.out.println(label + " mod " + a.lastModifiedTime() + " acc " + a.lastAccessTime()); + } + + public static void main(String[] args) throws IOException { + Path dir = Files.createTempDirectory("symlink-times"); + try { + Path file = dir.resolve("file"); + Path link = dir.resolve("link"); + Files.createFile(file); + Files.createSymbolicLink(link, file); + BasicFileAttributes before = read(file); + + FileTime mod = FileTime.fromMillis(1_000_060_000L); + FileTime acc = FileTime.fromMillis(1_000_120_000L); + var linkView = Files.getFileAttributeView(link, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + linkView.setTimes(mod, acc, null); + show("link", read(link, LinkOption.NOFOLLOW_LINKS)); + BasicFileAttributes after = read(file); + System.out.println("target unchanged " + after.lastModifiedTime().equals(before.lastModifiedTime()) + + " " + after.lastAccessTime().equals(before.lastAccessTime())); + + // each time alone keeps the other + linkView.setTimes(null, FileTime.fromMillis(2_000_000_000L), null); + show("link acc only", read(link, LinkOption.NOFOLLOW_LINKS)); + linkView.setTimes(FileTime.fromMillis(3_000_000_000L), null, null); + show("link mod only", read(link, LinkOption.NOFOLLOW_LINKS)); + + // the file itself, with nanoseconds + var view = Files.getFileAttributeView(file, BasicFileAttributeView.class); + view.setTimes(FileTime.from(Instant.ofEpochSecond(1_500_000_000L, 123_456_789)), + FileTime.from(Instant.ofEpochSecond(1_600_000_000L, 987_654_321)), null); + show("file", read(file)); + show("link", read(link, LinkOption.NOFOLLOW_LINKS)); + } finally { + Files.deleteIfExists(dir.resolve("link")); + Files.deleteIfExists(dir.resolve("file")); + Files.deleteIfExists(dir); + } + } +} diff --git a/test/java/WatchHostChanges.java b/test/java/WatchHostChanges.java new file mode 100644 index 0000000..92957f2 --- /dev/null +++ b/test/java/WatchHostChanges.java @@ -0,0 +1,38 @@ +import java.nio.file.*; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +// a host process changes a watched directory, then the program itself does +public class WatchHostChanges { + static void collect(java.nio.file.WatchService ws, TreeSet seen, long quietMs) throws Exception { + for (WatchKey key; (key = ws.poll(quietMs, TimeUnit.MILLISECONDS)) != null; key.reset()) { + for (WatchEvent e : key.pollEvents()) seen.add(e.kind().name() + " " + e.context()); + } + } + + public static void main(String[] args) throws Exception { + Path dir = Files.createTempDirectory("watch-host"); + try (java.nio.file.WatchService ws = FileSystems.getDefault().newWatchService()) { + dir.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); + String d = dir.toString(); + Process p = new ProcessBuilder("/bin/sh", "-c", + "touch '" + d + "/x'; sleep 0.3; echo hi >> '" + d + "/y'; sleep 0.3; rm '" + d + "/x'").start(); + p.waitFor(); + TreeSet host = new TreeSet<>(); + collect(ws, host, 1500); + System.out.println("host " + host); + + Files.writeString(dir.resolve("z"), "z"); + Files.delete(dir.resolve("y")); + TreeSet own = new TreeSet<>(); + collect(ws, own, 1500); + System.out.println("own " + own); + } finally { + try (var list = Files.list(dir)) { + for (Path f : list.toList()) Files.deleteIfExists(f); + } + Files.deleteIfExists(dir); + } + } +} diff --git a/test/jtreg/passing.txt b/test/jtreg/passing.txt index 2ca628c..9f3f945 100644 --- a/test/jtreg/passing.txt +++ b/test/jtreg/passing.txt @@ -860,6 +860,8 @@ java/lang/Package/PackageVersionTest.java java/lang/PrimitiveSumMinMaxTest.java java/lang/Process/WaitFor.java java/lang/Process/WaitForDuration.java +java/lang/ProcessBuilder/Basic.java#2 +java/lang/ProcessBuilder/Basic.java#3 java/lang/ProcessBuilder/CloseRace.java java/lang/ProcessBuilder/DestroyTest.java java/lang/ProcessBuilder/InheritIOEHandle.java @@ -1939,6 +1941,7 @@ java/nio/Buffer/Chars.java java/nio/Buffer/Chew.java java/nio/Buffer/CopyDirectMemory.java java/nio/Buffer/GetChars.java +java/nio/Buffer/LimitDirectMemory.java java/nio/Buffer/Order.java java/nio/Buffer/ReachabilityTest.java java/nio/Buffer/StringCharBufferSliceTest.java @@ -2201,6 +2204,7 @@ java/nio/file/Files/NotADirectory.java java/nio/file/Files/ReadWriteString.java java/nio/file/Files/SetLastModifiedTime.java java/nio/file/Files/StreamTest.java +java/nio/file/Files/SymlinkTime.java java/nio/file/Files/TemporaryFiles.java java/nio/file/Files/probeContentType/ForceLoad.java java/nio/file/Files/probeContentType/ParallelProbes.java @@ -2215,6 +2219,7 @@ java/nio/file/Path/ToRealPath.java java/nio/file/Path/UriImportExport.java java/nio/file/PathMatcher/Basic.java java/nio/file/WatchService/Basic.java +java/nio/file/WatchService/DeleteInterference.java java/nio/file/WatchService/LotsOfCancels.java java/nio/file/WatchService/LotsOfCloses.java java/nio/file/WatchService/LotsOfEntries.java @@ -2225,6 +2230,7 @@ java/nio/file/WatchService/UpdateInterference.java java/nio/file/attribute/AclEntry/EmptySet.java java/nio/file/attribute/AclFileAttributeView/Basic.java java/nio/file/attribute/BasicFileAttributeView/Basic.java +java/nio/file/attribute/BasicFileAttributeView/UnixSocketFile.java java/nio/file/attribute/DosFileAttributeView/Basic.java java/nio/file/attribute/FileTime/Basic.java java/nio/file/attribute/PosixFileAttributeView/Basic.java diff --git a/test/jtreg/times.tsv b/test/jtreg/times.tsv index 3add4d1..1e7f58b 100644 --- a/test/jtreg/times.tsv +++ b/test/jtreg/times.tsv @@ -36,12 +36,14 @@ 67326 java/security/KeyStore/TestKeyStoreBasic.java 65745 java/net/Socket/asyncClose/AsyncClose.java 65693 java/security/MessageDigest/ThreadSafetyTest.java +64636 java/lang/ProcessBuilder/Basic.java#2 63707 java/net/httpclient/LargeResponseContent.java 63593 java/security/SecureRandom/ThreadSafe.java 63402 java/beans/Performance/Test4058433.java 63310 java/net/Socket/Timeouts.java 63072 java/nio/channels/FileChannel/CleanerTest.java 62820 java/lang/invoke/VarHandles/VarHandleTestMethodTypeFloat.java +60194 java/lang/ProcessBuilder/Basic.java#3 59689 java/security/cert/CertPathValidator/OCSP/OCSPTimeout.java 59400 java/time/test/java/time/chrono/TestThaiBuddhistChronoImpl.java 59012 java/util/NavigableMap/LockStep.java @@ -160,6 +162,7 @@ 24155 java/util/concurrent/atomic/LongAdderDemo.java 24125 java/lang/invoke/VarHandles/VarHandleTestAccessString.java 24120 java/net/httpclient/http2/Timeout.java +23979 java/nio/Buffer/LimitDirectMemory.java 23424 java/lang/invoke/BigArityTest.java 23286 java/lang/RuntimeTests/loadLibrary/LoadLibraryTest.java 23282 java/util/Collection/HotPotatoes.java @@ -518,6 +521,7 @@ 9654 java/util/List/NestedSubList.java 9629 java/util/Calendar/CalendarLeapYearRollTest.java 9610 java/lang/IntegralPrimitiveToString.java +9609 java/nio/file/WatchService/DeleteInterference.java 9603 java/text/Collator/RuleBasedCollatorTest.java 9602 java/util/jar/JarFile/VerifySignedJar.java 9600 java/lang/Thread/virtual/CarrierThreadInfo.java#4 @@ -756,6 +760,7 @@ 7531 java/lang/reflect/Method/MethodArityLimit.java 7525 javax/crypto/KeyGenerator/TestGetInstance.java 7521 java/util/concurrent/CompletableFuture/ThenComposeAsyncTest.java +7514 java/nio/file/Files/SymlinkTime.java 7508 java/util/Map/LockStep.java 7500 java/util/TimeZone/TimeZoneBoundaryTest.java 7500 java/security/KeyStore/PKCS12/ReadP12Test.java @@ -3228,6 +3233,7 @@ 1948 java/lang/Boolean/ParseBoolean.java 1947 java/io/PrintWriter/FailingConstructors.java 1946 java/lang/Class/attributes/ClassAttributesTest.java +1945 java/nio/file/attribute/BasicFileAttributeView/UnixSocketFile.java 1944 java/lang/annotation/AnnotationTypeMismatchException/ArityTypeMismatchTest.java 1943 java/text/BreakIterator/Bug4912404.java 1943 java/nio/channels/SocketChannel/Basic.java diff --git a/test/programs.test.ts b/test/programs.test.ts index 62dd60a..0f0c4ab 100644 --- a/test/programs.test.ts +++ b/test/programs.test.ts @@ -33,7 +33,7 @@ for (const file of files) { const stdout: Uint8Array[] = []; const stderr: Uint8Array[] = []; let code: number; - if (vmFlags.some((f) => f.startsWith("-Xmx"))) { + if (vmFlags.some((f) => /^-(Xmx|XX:(OnOutOfMemoryError|MaxDirectMemorySize)=)/.test(f))) { const got = spawnSync(process.execPath, [join(root, "src", "node.ts"), ...vmFlags, "-cp", out, main], { input, encoding: "utf8" }); stdout.push(new TextEncoder().encode(got.stdout)); stderr.push(new TextEncoder().encode(got.stderr)); diff --git a/types/node.d.ts b/types/node.d.ts index 24afbf2..18e2ebf 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -22,6 +22,12 @@ export interface NodeRunOptions { // the process exits when its resident memory goes past this maxRssMb?: number; enablePreview?: boolean; + // -XX:OnOutOfMemoryError commands, run through /bin/sh on the first OutOfMemoryError the VM throws + onOutOfMemoryError?: string[]; + // -XX:+DisplayVMOutputToStderr: the VM's own messages go to stderr, not stdout + vmOutputToStderr?: boolean; + // -XX:MaxDirectMemorySize in bytes + maxDirectMemory?: number; // what InetAddress.getLocalHost() reports instead of the host's name hostName?: string; // what System.getenv() returns instead of the host's environment