diff --git a/docs/development.md b/docs/development.md index 38f8176..597a608 100644 --- a/docs/development.md +++ b/docs/development.md @@ -44,6 +44,10 @@ node scripts/jtreg/run.ts java/lang without the `+build` part. A sparse checkout of `test/lib` and the test directories is enough. +The runner evaluates a test's `@requires` as jtreg does on a release Linux +HotSpot with default options (for example `vm.continuations` is true and +`vm.debug` is false); a test that needs more is skipped. + `test/jtreg/passing.txt` lists the tests CI runs, with `--only-passing`. `--shard k/n` splits them into n shards of about equal run time, using each test's time on java-web from `test/jtreg/times.tsv` (the median for a test diff --git a/docs/support.md b/docs/support.md index 01cbb1d..504cb00 100644 --- a/docs/support.md +++ b/docs/support.md @@ -4,7 +4,8 @@ java-web targets headless Java: console apps, libraries and javac. - The image has every Java SE module (`java.se`) plus `jdk.compiler`, `jdk.httpserver`, `jdk.jartool` (the `jar` tool), `jdk.security.jgss`, - `jdk.unsupported`, `jdk.zipfs`, `jdk.internal.md` (Markdown in doc comments, + `jdk.management` (`UnixOperatingSystemMXBean`), `jdk.unsupported`, + `jdk.zipfs`, `jdk.internal.md` (Markdown in doc comments, for javac's doc comment API) and `jdk.localedata` (see Locales). - `java.desktop`'s headless parts work: 2D geometry, image pixels, `Graphics2D` drawing on a `BufferedImage`, and JavaBeans. So does `java.xml`, including @@ -21,6 +22,16 @@ java-web targets headless Java: console apps, libraries and javac. 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. +- Symbolic and hard links work on file systems that come from the host, as + under Node. Elsewhere, such as the browser's in-memory files, creating a link + fails with "Function not implemented". Files on host file systems have the + 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` + has no live locals or operands. `ThreadMXBean.getThreadInfo` and + `dumpThreads` are not implemented, and a virtual thread that yields while it + holds a monitor stays pinned to its carrier. - `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. diff --git a/scripts/build-jdk.sh b/scripts/build-jdk.sh index 2196547..0ca65a9 100644 --- a/scripts/build-jdk.sh +++ b/scripts/build-jdk.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "$0")/.." out=build/jdk -modules=${JDK_MODULES:-java.se,jdk.compiler,jdk.httpserver,jdk.jartool,jdk.security.jgss,jdk.unsupported,jdk.zipfs,jdk.localedata,jdk.internal.md,javaweb.tls} +modules=${JDK_MODULES:-java.se,jdk.compiler,jdk.httpserver,jdk.jartool,jdk.security.jgss,jdk.unsupported,jdk.zipfs,jdk.localedata,jdk.management,jdk.internal.md,javaweb.tls} # Builds from one pinned JDK release so every machine makes the same image. # Renovate bumps the release. diff --git a/scripts/jtreg/run.ts b/scripts/jtreg/run.ts index 7d8dee1..958b9e4 100644 --- a/scripts/jtreg/run.ts +++ b/scripts/jtreg/run.ts @@ -14,6 +14,7 @@ import { createHash } from "node:crypto"; import { spawn, spawnSync } from "node:child_process"; import type { ChildProcess } from "node:child_process"; import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { totalmem } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { hotspot } from "../hotspot.ts"; @@ -55,6 +56,89 @@ if (!existsSync(testRoot)) { } const javaHome = dirname(dirname(realpathSync(hotspot("java")))); +// ---- @requires -------------------------------------------------------------- + +// What jtreg reports for a release Linux HotSpot with default options. A +// property not listed makes the test skip. +const required: Record = { + "os.family": "linux", + "os.arch": process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "amd64" : process.arch, + "os.maxMemory": String(totalmem()), + "sun.arch.data.model": "64", + "vm.bits": "64", + "vm.continuations": "true", + "vm.flagless": "true", + "vm.debug": "false", + "vm.musl": "false", + "vm.compMode": "Xmixed", + "vm.gc": "null", + "vm.gc.G1": "true", + "vm.gc.Serial": "true", + "vm.gc.Parallel": "true", + "vm.gc.Z": "false", + "vm.hasJFR": "true", + "vm.graal.enabled": "false", + "vm.cpu.features": "", +}; + +function size(s: string): number { + const m = s.match(/^(\d+)([kmgt]?)$/i); + return m ? Number(m[1]) * 1024 ** (m[2] ? "kmgt".indexOf(m[2].toLowerCase()) + 1 : 0) : NaN; +} + +// Evaluates a jtreg @requires expression: & | ! ( ) and == != >= <= ~= over properties. +function requiresHolds(expr: string): boolean { + const toks = expr.match(/"[^"]*"|==|!=|>=|<=|~=|[()&|!<>]|[^\s()&|!=<>~"]+/g) ?? []; + let i = 0; + const value = (): string | null => { + const t = toks[i++]; + if (t === undefined) throw new Error("incomplete"); + if (t.startsWith('"')) return t.slice(1, -1); + if (/^[\d.]+[kmgt]?$/i.test(t) || t === "true" || t === "false") return t; + if (t.startsWith("vm.opt.")) return null; + if (t.startsWith("vm.gc.") && !(t in required)) return "false"; + if (t in required) return required[t]; + throw new Error(`unknown property ${t}`); + }; + const atom = (): boolean => { + if (toks[i] === "(") { + i++; + const v = or(); + if (toks[i++] !== ")") throw new Error("missing )"); + return v; + } + if (toks[i] === "!") { + i++; + return !atom(); + } + const a = value(); + const op = toks[i]; + if (!op || !/^(==|!=|>=|<=|~=|<|>)$/.test(op)) return a === "true"; + i++; + const b = value(); + switch (op) { + case "==": return a === b; + case "!=": return a !== b; + case "~=": return a !== null && new RegExp(`^(?:${b ?? ""})$`).test(a); + } + const x = size(a ?? ""), y = size(b ?? ""); + return op === ">=" ? x >= y : op === "<=" ? x <= y : op === ">" ? x > y : x < y; + }; + const and = (): boolean => { + let v = atom(); + while (toks[i] === "&") { i++; const r = atom(); v = v && r; } + return v; + }; + const or = (): boolean => { + let v = and(); + while (toks[i] === "|") { i++; const r = and(); v = v || r; } + return v; + }; + const v = or(); + if (i < toks.length) throw new Error("trailing input"); + return v; +} + type Kind = "main" | "testng" | "junit" | "compile"; // a compile action keeps its javac options in opts and its files in args interface Action { kind: Kind; opts: string[]; main: string; args: string[]; timeout: number } @@ -140,7 +224,13 @@ function parseFile(file: string): TestCase[] { case "build": tc.build.push(...value.split(/\s+/).filter(Boolean)); break; case "modules": tc.modules.push(...value.split(/\s+/).filter(Boolean)); break; case "enablePreview": tc.preview = true; break; - case "requires": tc.skip ??= "requires"; break; + case "requires": + try { + if (!requiresHolds(value)) tc.skip ??= "requires"; + } catch (e) { + tc.skip ??= `requires: ${(e as Error).message}`; + } + break; case "ignore": tc.skip ??= "ignore"; break; case "clean": tc.skip ??= "@clean"; break; case "compile": { @@ -341,7 +431,7 @@ function exec(cmd: string, args: string[], cwd: string, limit: number): Promise< const start = Date.now(); const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], detached: true }); let text = ""; - const keep = (d: Buffer) => { text = (text + d.toString()).slice(-4000); }; + const keep = (d: Buffer) => { text = (text + d.toString()).slice(-65536); }; child.stdout.on("data", keep); child.stderr.on("data", keep); let timedOut = false; diff --git a/src/fs.ts b/src/fs.ts index 9253812..72eed71 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -20,6 +20,11 @@ export interface Stat { mode?: number; // 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 + link?: number; + // the host's inode number and link count, so hard links and links to a file share a key + ino?: number; + nlink?: number; } // The same in every mount layout, so SecureRandom and the like work in the @@ -29,6 +34,7 @@ export const DEVICES = new Set(["/dev/null", "/dev/zero", "/dev/random", "/dev/u // Errors carry an errno name in `code`, as Node's do. export interface FileHandle { size(): number; + ino(): number; read(dst: Uint8Array, off: number, len: number, pos: number): number; write(src: Uint8Array, pos: number): void; truncate(size: number): void; @@ -55,7 +61,7 @@ export interface Backend { // 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): boolean; + setMtime?(path: string, ms: number, noFollow?: boolean): 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) @@ -63,6 +69,13 @@ export interface Backend { chmodError?(path: string): string | undefined; // whether the host allows reading (4) and executing (1), as access(2) access?(path: string, mode: number): boolean; + // symbolic and hard links, for a host that has them; failures return the errno code + lstat?(path: string): Stat | null; + readlink?(path: string): { target: string } | { code: string }; + symlink?(target: string, path: string): string | undefined; + 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; } export interface WarmTrace { @@ -309,6 +322,7 @@ export class VFS { // writes through the handle are changes watchers see, as writes through VFS.write are return { size: () => h.size(), + ino: () => h.ino(), read: (dst, off, len, pos) => h.read(dst, off, len, pos), close: () => h.close(), write: (src, pos) => { h.write(src, pos); this.changed(path, "modify"); }, @@ -407,6 +421,45 @@ export class VFS { return !!r && r[0].writable; } + // like stat, but a symbolic link reports itself + lstat(path: string): Stat | null { + const r = this.resolve(path); + return (r && r[0].lstat?.(r[1])) || this.stat(path); + } + + readlink(path: string): { target: string } | { code: string } { + const r = this.resolve(path); + return r?.[0].readlink?.(r[1]) ?? { code: this.stat(path) ? "EINVAL" : "ENOENT" }; + } + + // undefined on success, else the errno code; ENOSYS where the backend has no links + symlink(target: string, path: string): string | undefined { + const r = this.resolve(path); + if (!r || !r[0].writable || !r[0].symlink) return "ENOSYS"; + const code = r[0].symlink(target, r[1]); + if (!code) this.changed(path, "create"); + return code; + } + + link(existing: string, path: string): string | undefined { + const a = this.resolve(existing), b = this.resolve(path); + if (!a || !b || a[0] !== b[0] || !b[0].writable || !b[0].link) return "ENOSYS"; + const code = b[0].link(a[1], b[1]); + if (!code) this.changed(path, "create"); + return code; + } + + realpath(path: string): string | null { + const r = this.resolve(path); + if (!r?.[0].realpath) return null; + const real = r[0].realpath(r[1]); + if (real === null) return null; + const n = normalize(path); + // a mount point itself maps to "/" in its backend + const base = n.slice(0, n.length - (r[1] === "/" ? 0 : r[1].length)); + return normalize(base + real); + } + // A file system without its own figures reports 4 KB blocks and no space, // as Linux does for virtual file systems such as /proc. space(path: string): Space { @@ -417,10 +470,11 @@ 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): boolean { + setMtime(path: string, ms: number, ns?: bigint, noFollow = false): boolean { const r = this.resolve(path); - if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms)) return false; + if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms, noFollow)) 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 diff --git a/src/natives/io.ts b/src/natives/io.ts index 238e40a..66f9db6 100644 --- a/src/natives/io.ts +++ b/src/natives/io.ts @@ -6,7 +6,7 @@ import { JThread, PendingThrow } from "../thread.ts"; import type { NativeGen } from "../thread.ts"; import type { JVM } from "../jvm.ts"; import type { FileHandle, Stat, VFS } from "../fs.ts"; -import { closeSockFd } from "../sockets.ts"; +import { closeSockFd, socksOf } from "../sockets.ts"; import { nonNull, u8 } from "./util.ts"; import type { Natives } from "./util.ts"; @@ -45,6 +45,10 @@ interface Contents { host?: FileHandle; hostWritable?: boolean; hostReadable?: boolean; + // a host descriptor that keeps the file's inode alive, as an open descriptor does on Linux + pin?: FileHandle; + // the host inode these bytes came from, where the host has them + ino?: number; } // Opens a file's host handle when it outgrows memory. Kept apart from the @@ -66,6 +70,8 @@ function closeHostsOnExit(vm: JVM) { for (const f of tab?.files.values() ?? []) { f.contents.host?.close(); f.contents.host = undefined; + f.contents.pin?.close(); + f.contents.pin = undefined; } }); } @@ -181,6 +187,8 @@ export class OpenFile { return false; } c.host = host; + c.pin?.close(); + c.pin = undefined; c.hostWritable = true; c.data = new Uint8Array(0); c.dirty = false; @@ -190,16 +198,18 @@ export class OpenFile { // a mapping keeps the file open after its descriptor closes retain() { this.contents.refs++; } release() { - if (--this.contents.refs === 0 && this.contents.host) { - this.contents.host.close(); - this.contents.host = undefined; - } + const c = this.contents; + if (--c.refs > 0) return; + c.host?.close(); + c.host = undefined; + c.pin?.close(); + c.pin = undefined; } close() { this.release(); } } // A read-only directory descriptor can be opened, but cannot be read as bytes. -class DirectoryFile extends OpenFile { +export class DirectoryFile extends OpenFile { constructor(path: string, size: number) { super(path, new Uint8Array(0), false); this.size = size; @@ -277,7 +287,8 @@ function table(t: JThread): FdTable { return tab; } -export function fileInode(t: JThread, path: string): number { +export function fileInode(t: JThread, path: string, st: Stat | null = t.vm.fs.stat(path)): number { + if (st?.ino !== undefined) return st.ino; let inodes = t.vm.state.get("inodes") as Map | undefined; if (!inodes) t.vm.state.set("inodes", (inodes = new Map())); const ino = inodes.get(path); @@ -316,14 +327,15 @@ export function openFile(t: JThread, path: string, write: boolean, truncate: boo tab.files.set(tab.next, new DirectoryFile(abs, st.size)); return tab.next++; } - const shared = st ? [...table(t).files.values()].find((o) => o.path === abs && o.constructor === OpenFile)?.contents : undefined; + const shared = st ? [...table(t).files.values()].find((o) => o.path === abs && o.constructor === OpenFile && (o.contents.ino === undefined || o.contents.ino === st.ino))?.contents : undefined; if (st?.device || (st && !shared && !write && st.size >= LAZY_MIN && fs.canReadRange(abs))) { const tab = table(t); tab.files.set(tab.next, st.device ? new DeviceFile(abs) : new LazyFile(abs, st.size, fs)); return tab.next++; } if (write && fs.denied(abs)) notFound(path, "Permission denied"); - if (st && !shared && !truncate && st.size >= STREAM_MIN && fs.canOpenFile(abs, write)) { + // a file too big to hold, or one that may be written but not read, goes through a host handle + if (st && !shared && !truncate && (st.size >= STREAM_MIN || (write && !fs.access(abs, 4))) && fs.canOpenFile(abs, write)) { const host = openHost(fs, abs, write, readable); if (host) { closeHostsOnExit(t.vm); @@ -355,6 +367,8 @@ export function openFile(t: JThread, path: string, write: boolean, truncate: boo Object.assign(shared, { host, hostWritable: w, hostReadable: r }); } const f = new OpenFile(abs, data, write, shared); + if (!shared) f.contents.ino = (st ?? fs.stat(abs))?.ino; + if (!f.contents.host && !f.contents.pin) pinInode(t, f.contents, abs, write, readable); // only a writer can grow a file, so read-only files never hold an opener if (write && !hostOpeners.has(f.contents) && fs.canOpenFile(abs, true)) { const vm = t.vm; @@ -375,6 +389,17 @@ export function openFile(t: JThread, path: string, write: boolean, truncate: boo return fd; } +// Only a writable host tree can lose the file while it is open, and only one +// that reports inode numbers can reuse them. +function pinInode(t: JThread, c: Contents, abs: string, write: boolean, readable: boolean) { + const fs = t.vm.fs; + if (c.ino === undefined || !fs.canOpenFile(abs, true)) return; + const pin = openHost(fs, abs, false, true) ?? (write ? openHost(fs, abs, true, readable) : undefined); + if (pin?.ino() !== c.ino) return pin?.close(); + c.pin = pin; + closeHostsOnExit(t.vm); +} + function openHost(fs: VFS, abs: string, write: boolean, readable: boolean): FileHandle | undefined { try { return fs.openFile(abs, write, false, readable); } catch { return undefined; } @@ -387,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): boolean { +export function setMtime(t: JThread, abs: string, ms: number, ns?: bigint, noFollow = false): boolean { syncPath(t, abs); - return t.vm.fs.setMtime(abs, ms, ns); + return t.vm.fs.setMtime(abs, ms, ns, noFollow); } // write(2) on a descriptor opened read-only fails with EBADF. @@ -439,6 +464,13 @@ export function writeFailed(t: JThread, path: string): PendingThrow { return new PendingThrow("java/io/IOException", ERRNO[t.vm.fs.writeError(path) ?? ""] ?? "Input/output error"); } +// Descriptors the program holds, with the three standard streams. +export function openFdCount(vm: JVM): number { + const files = (vm.state.get("fds") as FdTable | undefined)?.files.size ?? 0; + const pipeCount = (vm.state.get("processPipes") as Map | undefined)?.size ?? 0; + return 3 + files + pipeCount + socksOf(vm.state).size; +} + export function newFd(t: JThread): number { return table(t).next++; } @@ -501,8 +533,8 @@ export function removePath(t: JThread, abs: string): boolean { } function detachFile(t: JThread, f: OpenFile) { - f.ino ??= fileInode(t, f.path); const stat = t.vm.fs.stat(f.path); + f.ino ??= fileInode(t, f.path, stat); if (stat) f.detached = { path: f.path, stat, writable: t.vm.fs.writable(f.path) }; } @@ -729,7 +761,10 @@ export const natives: Natives = { f.setLength(Number(n)); }, - [`${UFS}.canonicalize0(Ljava/lang/String;)Ljava/lang/String;`]: (t, [, p]) => t.vm.str(resolvePath(t, t.vm.jsString(p))), + [`${UFS}.canonicalize0(Ljava/lang/String;)Ljava/lang/String;`]: (t, [, p]) => { + const abs = resolvePath(t, t.vm.jsString(p)); + return t.vm.str(t.vm.fs.realpath(abs) ?? abs); + }, [`${UFS}.getBooleanAttributes0(Ljava/io/File;)I`]: (t, [, f]) => attrs(t, f), [`${UFS}.checkAccess0(Ljava/io/File;I)Z`]: (t, [, f, access]) => { const abs = resolvePath(t, pathOf(t, f)); @@ -752,8 +787,9 @@ export const natives: Natives = { return chmod(t, f, (m) => (enable ? m | bits : m & ~bits)); }, [`${UFS}.createFileExclusively0(Ljava/lang/String;)Z`]: (t, [, p]) => { + if (t.vm.jsString(p) === "") throw new PendingThrow("java/io/IOException", "No such file or directory"); const abs = resolvePath(t, t.vm.jsString(p)); - if (t.vm.fs.stat(abs)) return 0; + if (t.vm.fs.lstat(abs)) return 0; if (t.vm.fs.denied(abs)) throw new PendingThrow("java/io/IOException", "Permission denied"); if (!t.vm.fs.write(abs, new Uint8Array(0))) throw new PendingThrow("java/io/IOException", "No such file or directory"); return 1; diff --git a/src/natives/management.ts b/src/natives/management.ts index 89af721..1d0bdff 100644 --- a/src/natives/management.ts +++ b/src/natives/management.ts @@ -10,6 +10,8 @@ import type { JMethod, JObject } from "../runtime.ts"; import { TERMINATED } from "../thread.ts"; import type { JThread, NativeGen } from "../thread.ts"; import type { JVM } from "../jvm.ts"; +import { openFdCount } from "./io.ts"; +import { box, initialized } from "./reflect.ts"; import type { Natives } from "./util.ts"; const MANAGERS = [ @@ -112,7 +114,37 @@ function liveThreads(vm: JVM): JThread[] { return [...vm.threads].filter((x) => x.state !== TERMINATED && x.obj); } +// A host module under Node; none in a browser. +function host(name: string): any { + return (globalThis as any).process?.getBuiltinModule?.(name); +} + +// A number from a /proc file's line that starts with the label, in its unit +function procValue(file: string, label: string, field = 1): number { + try { + const line = host("node:fs").readFileSync(file, "utf8").split("\n").find((l: string) => l.startsWith(label)); + return Number(line?.trim().split(/\s+/)[field]); + } catch { + return NaN; + } +} + +const orElse = (x: number, fallback: number) => (Number.isFinite(x) ? x : fallback); +const KB = 1024; + +// The VM options java-web can state, as HotSpot reports them with the Serial collector +const FLAGS: { name: string; value: boolean | number; writeable: boolean }[] = [ + { name: "HeapDumpOnOutOfMemoryError", value: false, writeable: true }, + { name: "LockingMode", value: 2, writeable: false }, + { name: "MaxHeapSize", value: MAX_HEAP, writeable: false }, + { name: "PrintConcurrentLocks", value: false, writeable: true }, + { name: "UseCompressedOops", value: true, writeable: false }, + { name: "UseParallelGC", value: false, writeable: false }, + { name: "UseSerialGC", value: true, writeable: false }, +]; + const V = "sun/management/VMManagementImpl."; +const OS = "com/sun/management/internal/OperatingSystemImpl."; const P = "sun/management/MemoryPoolImpl."; const T = "sun/management/ThreadImpl."; @@ -209,4 +241,51 @@ export const natives: Natives = { "sun/management/HotspotThread.getInternalThreadCount()I": () => 0, "sun/management/HotspotThread.getInternalThreadTimes0([Ljava/lang/String;[J)I": () => 0, "sun/management/ClassLoadingImpl.setVerboseClass(Z)V": () => undefined, + + // no container limits: Container.metrics() is null + "jdk/internal/platform/CgroupMetrics.isUseContainerSupport()Z": () => 0, + "jdk/internal/platform/CgroupMetrics.isContainerized0()Z": () => 0, + "jdk/internal/platform/CgroupMetrics.getTotalMemorySize0()J": () => BigInt(host("node:os")?.totalmem() ?? MAX_HEAP), + "jdk/internal/platform/CgroupMetrics.getTotalSwapSize0()J": () => BigInt(orElse(procValue("/proc/meminfo", "SwapTotal:") * KB, 0)), + + "com/sun/management/internal/Flag.initialize()V": () => undefined, + "com/sun/management/internal/Flag.getInternalFlagCount()I": () => FLAGS.length, + "com/sun/management/internal/Flag.getAllFlagNames()[Ljava/lang/String;": (t) => t.vm.newObjectArray("java/lang/String", FLAGS.map((f) => t.vm.str(f.name))), + "com/sun/management/internal/Flag.getFlags([Ljava/lang/String;[Lcom/sun/management/internal/Flag;I)I": function* (t, [names, out, count]): NativeGen { + const vm = t.vm; + const cls = vm.loadBoot("com/sun/management/internal/Flag")!; + const origin = yield* initialized(t, "com/sun/management/VMOption$Origin"); + const defaultOrigin = yield t.call(origin.methodMap.get("valueOf(Ljava/lang/String;)Lcom/sun/management/VMOption$Origin;")!, vm.str("DEFAULT")); + const wanted = names ? (names.a as (JObject | null)[]).map((n) => (n ? vm.jsString(n) : null)) : FLAGS.map((f) => f.name); + let filled = 0; + for (let i = 0; i < Math.min(count, wanted.length); i++) { + const f = FLAGS.find((x) => x.name === wanted[i]); + if (!f) continue; + const value = typeof f.value === "boolean" ? yield* box(t, "Z", f.value ? 1 : 0) : yield* box(t, "J", BigInt(f.value)); + const flag = vm.newObject(cls); + yield t.call(cls.methodMap.get("(Ljava/lang/String;Ljava/lang/Object;ZZLcom/sun/management/VMOption$Origin;)V")!, flag, vm.str(f.name), value, f.writeable ? 1 : 0, 1, defaultOrigin); + (out.a as (JObject | null)[])[i] = flag; + filled++; + } + return filled; + }, + + [`${OS}initialize0()V`]: () => undefined, + [`${OS}getOpenFileDescriptorCount0()J`]: (t) => BigInt(openFdCount(t.vm)), + [`${OS}getMaxFileDescriptorCount0()J`]: () => BigInt(orElse(procValue("/proc/self/limits", "Max open files", 3), 1024)), + [`${OS}getCommittedVirtualMemorySize0()J`]: () => BigInt(orElse(procValue("/proc/self/statm", "", 0) * 4096, (globalThis as any).process?.memoryUsage?.().rss ?? -1)), + [`${OS}getTotalMemorySize0()J`]: () => BigInt(host("node:os")?.totalmem() ?? MAX_HEAP), + [`${OS}getFreeMemorySize0()J`]: () => BigInt(host("node:os")?.freemem() ?? 0), + [`${OS}getTotalSwapSpaceSize0()J`]: () => BigInt(orElse(procValue("/proc/meminfo", "SwapTotal:") * KB, 0)), + [`${OS}getFreeSwapSpaceSize0()J`]: () => BigInt(orElse(procValue("/proc/meminfo", "SwapFree:") * KB, 0)), + [`${OS}getProcessCpuTime0()J`]: () => { + const u = (globalThis as any).process?.cpuUsage?.(); + return u ? BigInt(u.user + u.system) * 1000n : -1n; + }, + [`${OS}getProcessCpuLoad0()D`]: () => -1, + [`${OS}getCpuLoad0()D`]: () => -1, + [`${OS}getSingleCpuLoad0(I)D`]: () => -1, + [`${OS}getHostConfiguredCpuCount0()I`]: () => host("node:os")?.availableParallelism() ?? 1, + [`${OS}getHostOnlineCpuCount0()I`]: () => host("node:os")?.availableParallelism() ?? 1, + [`${OS}getHostTotalCpuTicks0()J`]: () => -1n, }; diff --git a/src/natives/nio.ts b/src/natives/nio.ts index 9bb212e..e5f7ea3 100644 --- a/src/natives/nio.ts +++ b/src/natives/nio.ts @@ -6,7 +6,7 @@ import type { Stat } from "../fs.ts"; import { preCloseSock } from "../sockets.ts"; import { closeInotify, sockRead, sockReadv, sockWrite, sockWritev } from "./sockets.ts"; import { JThread, PendingThrow } from "../thread.ts"; -import { closeFd, endsInDot, fileInode, fileOf, flush, flushOrThrow, openFile, removePath, renamePath, resolvePath, setMtime, syncPath, writeFailed } from "./io.ts"; +import { closeFd, DirectoryFile, newFd, fileInode, fileOf, flush, flushOrThrow, openFile, removePath, renamePath, resolvePath, setMtime, syncPath, writeFailed } from "./io.ts"; import type { OpenFile } from "./io.ts"; import type { Natives } from "./util.ts"; @@ -27,13 +27,13 @@ const MESSAGES: Record = { // errno numbers of the host errors Node reports by name const HOST_ERRNO: Record = { - EPERM, ENOENT, EINTR, EIO, EBADF, ENOMEM, EACCES, ENOTDIR, EINVAL, ENOSPC, EROFS, ELOOP, + EPERM, ENOENT, EINTR, EIO, EBADF, ENOMEM, EACCES, EEXIST, EXDEV, ENOTDIR, EISDIR, EINVAL, ENOSPC, EROFS, ELOOP, ENOTEMPTY, ENOSYS, ENOTSUP, EOPNOTSUPP: ENOTSUP, ESTALE, ENAMETOOLONG, EDQUOT, }; // Linux open(2) flags -const O_ACCMODE = 3, O_CREAT = 0o100, O_EXCL = 0o200, O_TRUNC = 0o1000, O_APPEND = 0o2000; -const S_IFDIR = 0o040000, S_IFREG = 0o100000, S_IFCHR = 0o020000; +const O_ACCMODE = 3, O_DIRECTORY = 0o200000, O_NOFOLLOW = 0o400000, O_CREAT = 0o100, O_EXCL = 0o200, O_TRUNC = 0o1000, O_APPEND = 0o2000; +const S_IFDIR = 0o040000, S_IFREG = 0o100000, S_IFCHR = 0o020000, S_IFLNK = 0o120000; export function unixError(errno: number): never { throw new PendingThrow("sun/nio/fs/UnixException", null, { desc: "(I)V", args: [errno] }); @@ -57,6 +57,76 @@ function path(t: JThread, addr: bigint) { return resolvePath(t, cstring(t, addr)); } +const SUPPORTS_OPENAT = 2; +const AT_SYMLINK_NOFOLLOW = 0x100, AT_FDCWD = -100, AT_REMOVEDIR = 0x200; + +// path(2) relative to the directory behind dfd, as the *at system calls resolve it +function pathAt(t: JThread, dfd: number, p: bigint): string { + const name = cstring(t, p); + if (dfd === AT_FDCWD || name.startsWith("/")) return resolvePath(t, name); + const dir = t.vm.state.get("fds")?.files.get(dfd); + if (!dir) unixError(EBADF); + if (!(dir instanceof DirectoryFile)) unixError(ENOTDIR); + if (!dir.path) unixError(ENOENT); + return resolvePath(t, `${dir.path}/${name}`); +} + +// the last path component, ignoring trailing slashes +function lastComponent(p: string): string | undefined { + return p.replace(/\/+$/, "").split("/").pop(); +} + +// the descriptor's file, or EBADF as the system calls give for one that is not open +function openFileOf(t: JThread, fd: number) { + const f = t.vm.state.get("fds")?.files.get(fd); + if (!f) unixError(EBADF); + return f; +} + +function removeDir(t: JThread, abs: string) { + const st = t.vm.fs.lstat(abs); + if (!st) unixError(ENOENT); + if (!st.dir) unixError(ENOTDIR); + if (!removePath(t, abs)) unixError(ENOTEMPTY); +} + +function unlinkAbs(t: JThread, abs: string) { + const st = t.vm.fs.lstat(abs); + if (!st) unixError(ENOENT); + if (st.dir && st.link === undefined) unixError(EISDIR); + if (!removePath(t, abs)) unixError(EROFS); +} + +function renameAbs(t: JThread, from: string, to: string) { + if (!t.vm.fs.lstat(from)) unixError(ENOENT); + if (!renamePath(t, from, to)) unixError(EXDEV); +} + +function openAbs(t: JThread, abs: string, flags: number, mode: number): number { + const st = t.vm.fs.stat(abs); + if ((flags & O_CREAT) && (flags & O_EXCL) && (st || t.vm.fs.lstat(abs))) unixError(EEXIST); + 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); + if ((write || !st) && t.vm.fs.denied(abs)) unixError(EACCES); + if (st && !st.dir && (flags & O_ACCMODE) !== 1 && !t.vm.fs.access(abs, 4)) unixError(EACCES); + try { + const fd = openFile(t, abs, write, (flags & O_TRUNC) !== 0, (flags & O_APPEND) !== 0, (flags & O_ACCMODE) !== 1, true); + fileOf(t, fd).ino = fileInode(t, abs); + if (!st) applyCreateMode(t, abs, mode); + return fd; + } catch (e) { + if (e instanceof PendingThrow) { + // a new file the host would not create: its reason + const host = st ? undefined : HOST_ERRNO[t.vm.fs.writeError(abs) ?? ""]; + unixError(host ?? (t.vm.fs.writable(abs) ? ENOENT : EROFS)); + } + throw e; + } +} + function chmod(t: JThread, p: string, mode: number) { const fs = t.vm.fs; const result = fs.chmod(p, (old) => (old & ~0o7777) | (mode & 0o7777)); @@ -71,17 +141,30 @@ function chmod(t: JThread, p: string, mode: number) { unixError(HOST_ERRNO[fs.chmodError(p) ?? "EIO"] ?? EIO); } -function fillAttrs(t: JThread, attrs: JObject, p: string): number { +function accountId(t: JThread, name: string): number { + if (name === "root") return 0; + return name === (t.vm.props.get("user.name") ?? "user") ? 1000 : -1; +} + +let processUmask: number | undefined; + +// open(2) and mkdir(2) give a new file the requested mode less the umask +function applyCreateMode(t: JThread, abs: string, mode: number) { + processUmask ??= (globalThis as any).process?.umask?.() ?? 0o022; + t.vm.fs.chmod(abs, (old) => (old & ~0o7777) | (mode & ~processUmask! & 0o7777)); +} + +function fillAttrs(t: JThread, attrs: JObject, p: string, follow = true): number { syncPath(t, p); - const st = t.vm.fs.stat(p); + const st = follow ? t.vm.fs.stat(p) : t.vm.fs.lstat(p); if (!st) return ENOENT; - fillStat(t, attrs, st, t.vm.fs.writable(p), fileInode(t, p)); + fillStat(t, attrs, st, t.vm.fs.writable(p), fileInode(t, p, st), st.size, st.nlink ?? 1); return 0; } 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.dir ? S_IFDIR : S_IFREG) | (st.mode ?? (st.dir ? (writable ? 0o755 : 0o555) : (writable ? 0o644 : 0o444))); + 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; @@ -107,7 +190,7 @@ function mountTables(t: JThread): Map { return m; } -interface Dir { names: string[]; pos: number } +interface Dir { names: string[]; pos: number; fd?: number } function dirs(t: JThread): Map { let m = t.vm.state.get("dirs"); @@ -120,12 +203,12 @@ let nextDir = 1n; const N = "sun/nio/fs/UnixNativeDispatcher."; export const natives: Natives = { - [`${N}init()I`]: () => 0, + [`${N}init()I`]: () => SUPPORTS_OPENAT, [`${N}getcwd()[B`]: (t) => bytes(t, t.vm.opts.cwd ?? "/"), [`${N}strerror(I)[B`]: (t, [errno]) => bytes(t, MESSAGES[errno] ?? `errno ${errno}`), [`${N}stat0(JLsun/nio/fs/UnixFileAttributes;)I`]: (t, [p, attrs]) => fillAttrs(t, attrs, path(t, p)), [`${N}lstat0(JLsun/nio/fs/UnixFileAttributes;)V`]: (t, [p, attrs]) => { - const err = fillAttrs(t, attrs, path(t, p)); + const err = fillAttrs(t, attrs, path(t, p), false); if (err) unixError(err); }, [`${N}fstat0(ILsun/nio/fs/UnixFileAttributes;)V`]: (t, [fd, attrs]) => { @@ -143,64 +226,71 @@ export const natives: Natives = { if (!t.vm.fs.stat(abs)) return ENOENT; return t.vm.fs.access(abs, amode) ? 0 : EACCES; }, - [`${N}open0(JII)I`]: (t, [p, flags]) => { - const abs = path(t, p); - const st = t.vm.fs.stat(abs); - if ((flags & O_CREAT) && (flags & O_EXCL) && st) unixError(EEXIST); - if (!st && !(flags & O_CREAT)) unixError(ENOENT); - const write = (flags & O_ACCMODE) !== 0; - if (st?.dir && write) unixError(EISDIR); - if ((write || !st) && t.vm.fs.denied(abs)) unixError(EACCES); - try { - const fd = openFile(t, abs, write, (flags & O_TRUNC) !== 0, (flags & O_APPEND) !== 0, (flags & O_ACCMODE) !== 1, true); - fileOf(t, fd).ino = fileInode(t, abs); - return fd; - } catch (e) { - if (e instanceof PendingThrow) { - // a new file the host would not create: its reason - const host = st ? undefined : HOST_ERRNO[t.vm.fs.writeError(abs) ?? ""]; - unixError(host ?? (t.vm.fs.writable(abs) ? ENOENT : EROFS)); - } - throw e; - } + [`${N}open0(JII)I`]: (t, [p, flags, mode]) => openAbs(t, path(t, p), flags, mode), + [`${N}openat0(IJII)I`]: (t, [dfd, p, flags, mode]) => openAbs(t, pathAt(t, dfd, p), flags, mode), + [`${N}unlinkat0(IJI)V`]: (t, [dfd, p, flag]) => { + const last = lastComponent(cstring(t, p)); + const abs = pathAt(t, dfd, p); + if (last === "." || last === "..") unixError(flag & AT_REMOVEDIR ? (last === "." ? EINVAL : ENOTEMPTY) : EISDIR); + if (flag & AT_REMOVEDIR) removeDir(t, abs); + else unlinkAbs(t, abs); + }, + [`${N}renameat0(IJIJ)V`]: (t, [sfd, a, dfd, b]) => renameAbs(t, pathAt(t, sfd, a), pathAt(t, dfd, b)), + [`${N}fstatat0(IJILsun/nio/fs/UnixFileAttributes;)V`]: (t, [dfd, p, flag, attrs]) => { + const err = fillAttrs(t, attrs, pathAt(t, dfd, p), !(flag & AT_SYMLINK_NOFOLLOW)); + if (err) unixError(err); + }, + [`${N}fchmodat0(IJII)V`]: (t, [dfd, p, mode]) => chmod(t, pathAt(t, dfd, p), mode), + [`${N}fdopendir(I)J`]: (t, [fd]) => { + const f = openFileOf(t, fd); + if (!t.vm.fs.stat(f.path)?.dir) unixError(ENOTDIR); + const id = nextDir++; + dirs(t).set(id, { names: t.vm.fs.list(f.path) ?? [], pos: 0, fd }); + return id; + }, + [`${N}dup(I)I`]: (t, [fd]) => { + const f = openFileOf(t, fd); + const copy = newFd(t); + t.vm.state.get("fds").files.set(copy, f); + f.retain(); + return copy; }, [`${N}close0(I)V`]: (t, [fd]) => { closeInotify(t, fd); closeFd(t, fd); }, - [`${N}dup(I)I`]: () => unixError(ENOSYS), - [`${N}mkdir0(JI)V`]: (t, [p]) => { + [`${N}mkdir0(JI)V`]: (t, [p, mode]) => { const abs = path(t, p); - if (t.vm.fs.stat(abs)) unixError(EEXIST); + if (t.vm.fs.lstat(abs)) unixError(EEXIST); if (!t.vm.fs.mkdir(abs)) unixError(t.vm.fs.writable(abs) ? ENOENT : EROFS); + applyCreateMode(t, abs, mode); }, [`${N}rmdir0(J)V`]: (t, [p]) => { - if (endsInDot(cstring(t, p))) unixError(EINVAL); - const abs = path(t, p); - const st = t.vm.fs.stat(abs); - if (!st) unixError(ENOENT); - if (!st.dir) unixError(ENOTDIR); - if (!removePath(t, abs)) unixError(ENOTEMPTY); - }, - [`${N}unlink0(J)V`]: (t, [p]) => { - const abs = path(t, p); - const st = t.vm.fs.stat(abs); - if (!st) unixError(ENOENT); - if (st.dir) unixError(EISDIR); - if (!removePath(t, abs)) unixError(EROFS); - }, - [`${N}rename0(JJ)V`]: (t, [a, b]) => { - if (!t.vm.fs.stat(path(t, a))) unixError(ENOENT); - if (!renamePath(t, path(t, a), path(t, b))) unixError(EXDEV); + const last = lastComponent(cstring(t, p)); + if (last === "." || last === "..") unixError(last === "." ? EINVAL : ENOTEMPTY); + removeDir(t, path(t, p)); }, + [`${N}unlink0(J)V`]: (t, [p]) => unlinkAbs(t, path(t, p)), + [`${N}rename0(JJ)V`]: (t, [a, b]) => renameAbs(t, path(t, a), path(t, b)), [`${N}realpath0(J)[B`]: (t, [p]) => { const abs = path(t, p); if (!t.vm.fs.stat(abs)) unixError(ENOENT); - return bytes(t, abs); + return bytes(t, t.vm.fs.realpath(abs) ?? abs); + }, + [`${N}readlink0(J)[B`]: (t, [p]) => { + const r = t.vm.fs.readlink(path(t, p)); + if ("code" in r) unixError(HOST_ERRNO[r.code] ?? EIO); + return bytes(t, r.target); + }, + [`${N}symlink0(JJ)V`]: (t, [target, p]) => { + const code = t.vm.fs.symlink(cstring(t, target), path(t, p)); + if (code) unixError(HOST_ERRNO[code] ?? EIO); + }, + [`${N}link0(JJ)V`]: (t, [existing, p]) => { + syncPath(t, path(t, existing)); + const code = t.vm.fs.link(path(t, existing), path(t, p)); + if (code) unixError(HOST_ERRNO[code] ?? EIO); }, - [`${N}readlink0(J)[B`]: () => unixError(EINVAL), - [`${N}symlink0(JJ)V`]: () => unixError(ENOSYS), - [`${N}link0(JJ)V`]: () => unixError(ENOSYS), [`${N}chmod0(JI)V`]: (t, [p, mode]) => { chmod(t, path(t, p), mode); }, @@ -221,10 +311,10 @@ export const natives: Natives = { [`${N}lchown0(JII)V`]: () => undefined, [`${N}fchown0(III)V`]: () => undefined, // times in nanoseconds; only the modification time is kept - [`${N}utimensat0(IJJJI)V`]: (t, [, p, , mtime]) => { - const abs = path(t, p); - if (!t.vm.fs.stat(abs)) unixError(ENOENT); - if (!setMtime(t, abs, Number(mtime) / 1e6, BigInt(mtime))) unixError(EROFS); + [`${N}utimensat0(IJJJI)V`]: (t, [dfd, p, , 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); }, [`${N}futimens0(IJJ)V`]: (t, [fd, , mtime]) => { const f = fileOf(t, fd); @@ -244,11 +334,16 @@ export const natives: Natives = { if (!d) unixError(EBADF); return d.pos < d.names.length ? bytes(t, d.names[d.pos++]) : null; }, - [`${N}closedir(J)V`]: (t, [id]) => void dirs(t).delete(id), - [`${N}getpwuid(I)[B`]: (t) => bytes(t, t.vm.props.get("user.name") ?? "user"), - [`${N}getgrgid(I)[B`]: (t) => bytes(t, t.vm.props.get("user.name") ?? "user"), - [`${N}getpwnam0(J)I`]: () => 1000, - [`${N}getgrnam0(J)I`]: () => 1000, + [`${N}closedir(J)V`]: (t, [id]) => { + const fd = dirs(t).get(id)?.fd; + dirs(t).delete(id); + if (fd !== undefined) closeFd(t, fd); + }, + [`${N}getpwuid(I)[B`]: (t, [id]) => bytes(t, id === 0 ? "root" : t.vm.props.get("user.name") ?? "user"), + [`${N}getgrgid(I)[B`]: (t, [id]) => bytes(t, id === 0 ? "root" : t.vm.props.get("user.name") ?? "user"), + // the user and root are the only accounts; -1 for any other name + [`${N}getpwnam0(J)I`]: (t, [p]) => accountId(t, cstring(t, p)), + [`${N}getgrnam0(J)I`]: (t, [p]) => accountId(t, cstring(t, p)), [`${N}fchmodatNoFollowSupported0()Z`]: () => 0, [`${N}statvfs0(JLsun/nio/fs/UnixFileStoreAttributes;)V`]: (t, [p, attrs]) => { const abs = path(t, p); diff --git a/src/natives/reflect.ts b/src/natives/reflect.ts index 71f067a..c0b1eed 100644 --- a/src/natives/reflect.ts +++ b/src/natives/reflect.ts @@ -58,7 +58,7 @@ function* exceptionTypes(t: JThread, m: JMethod): NativeGen { return t.vm.newObjectArray("java/lang/Class", out); } -function* initialized(t: JThread, name: string): NativeGen { +export function* initialized(t: JThread, name: string): NativeGen { const c = t.vm.loadBoot(name)!; if (c.state !== INITIALIZED) yield* t.vm.initGen(t, c); return c; diff --git a/src/node.ts b/src/node.ts index 5e7fd59..1e9d9a6 100644 --- a/src/node.ts +++ b/src/node.ts @@ -40,7 +40,7 @@ export class NodeBackend implements Backend { return this.root === "/" ? p : this.root + p; } openFile(p: string, write: boolean, truncate: boolean, readable: boolean) { - const flags = write ? (readable ? fs.constants.O_RDWR : fs.constants.O_WRONLY) | fs.constants.O_CREAT | (truncate ? fs.constants.O_TRUNC : 0) : fs.constants.O_RDONLY; + const flags = write ? (readable ? fs.constants.O_RDWR : fs.constants.O_WRONLY) | (truncate ? fs.constants.O_TRUNC : 0) : fs.constants.O_RDONLY; let fd: number; try { const host = this.host(p); @@ -58,6 +58,7 @@ export class NodeBackend implements Backend { } return { size: () => fs.fstatSync(fd).size, + ino: () => fs.fstatSync(fd).ino, read: (dst: Uint8Array, off: number, len: number, pos: number) => fs.readSync(fd, dst, off, len, pos), write: (src: Uint8Array, pos: number) => { let n = 0; @@ -82,13 +83,57 @@ 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 }; + st = { dir: s.isDirectory(), size: s.size, mtime: s.mtimeMs, mode: s.mode & 0o7777, ino: s.ino, nlink: s.nlink }; } catch { st = null; } if (!this.writable) this.stats.set(p, st); return st; } + lstat(p: string): Stat | null { + 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 }; + } catch { + return null; + } + } + readlink(p: string) { + try { + return { target: fs.readlinkSync(this.host(p)) }; + } catch (e) { + return { code: (e as NodeJS.ErrnoException).code ?? "EIO" }; + } + } + symlink(target: string, p: string) { + try { + fs.symlinkSync(target, this.host(p)); + } catch (e) { + return (e as NodeJS.ErrnoException).code ?? "EIO"; + } + } + link(existing: string, p: string) { + try { + fs.linkSync(this.host(existing), this.host(p)); + } catch (e) { + return (e as NodeJS.ErrnoException).code ?? "EIO"; + } + } + realpath(p: string) { + const host = this.host(p); + const rest: string[] = []; + for (let dir = host; ; dir = path.dirname(dir)) { + try { + const real = fs.realpathSync.native(dir); + const full = path.join(real, ...rest); + return this.root === "/" ? full : full.startsWith(this.root) ? full.slice(this.root.length) || "/" : null; + } catch (e) { + if (dir === path.dirname(dir) || (e as NodeJS.ErrnoException).code === "EACCES") return null; + rest.unshift(path.basename(dir)); + } + } + } // Only regular files: readFileSync on a device or pipe can grow without end. read(p: string) { try { @@ -187,9 +232,10 @@ export class NodeBackend implements Backend { return null; } } - setMtime(p: string, ms: number) { + setMtime(p: string, ms: number, noFollow = false) { try { - fs.utimesSync(this.host(p), fs.statSync(this.host(p)).atime, ms / 1000); + 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); return true; } catch { return false; @@ -206,7 +252,7 @@ export class NodeBackend implements Backend { // like unlink or rmdir: a directory must be empty remove(p: string) { try { - if (fs.statSync(this.host(p)).isDirectory()) fs.rmdirSync(this.host(p)); + if (fs.lstatSync(this.host(p)).isDirectory()) fs.rmdirSync(this.host(p)); else fs.rmSync(this.host(p)); return true; } catch { diff --git a/src/process-node.ts b/src/process-node.ts index 1cac603..3a17bcd 100644 --- a/src/process-node.ts +++ b/src/process-node.ts @@ -16,6 +16,28 @@ const drained = (output: Writable) => output.writableNeedDrain ? new Promise((resolve) => { output.once("drain", resolve); output.once("close", resolve); }) : undefined; +// execvp as the JDK's launcher does it: the parent's PATH, ":/bin:/usr/bin" when it has none, +// and EACCES in place of ENOENT when a match was not executable. +function findProgram(command: string, cwd: string, parentPath: string | undefined): string | NodeJS.ErrnoException { + if (command === "") return Object.assign(new Error("ENOENT"), { code: "ENOENT", errno: -2 }); + if (command.includes("/")) return command; + let denied = false; + for (const dir of (parentPath ?? ":/bin:/usr/bin").split(":")) { + const candidate = dir ? `${dir}/${command}` : `./${command}`; + const full = path.resolve(cwd, candidate); + try { + if (!fs.statSync(full).isFile()) denied = true; + else { + fs.accessSync(full, fs.constants.X_OK); + return candidate; + } + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "EACCES") denied = true; + } + } + return Object.assign(new Error(denied ? "EACCES" : "ENOENT"), { code: denied ? "EACCES" : "ENOENT", errno: denied ? -13 : -2 }); +} + class Pipe implements ProcessPipe { private chunks: Uint8Array[] = []; private length = 0; @@ -171,8 +193,8 @@ export function processNatives(entry: string, jdk: string, packaged: boolean, ho args = [entry, "--java-child", ...(packaged ? [] : ["--jdk", jdk]), ...args]; } const env = envBlock === null ? t.vm.opts.env : Object.fromEntries(block(envBlock).map((s) => { - const i = s.indexOf("="); - return [s.slice(0, i), s.slice(i + 1)]; + const i = s.indexOf("=", 1); + return i < 0 ? [s, ""] : [s.slice(0, i), s.slice(i + 1)]; })); const descriptors = (fds as JArray).a as Int32Array; const opened: number[] = []; @@ -207,7 +229,12 @@ export function processNatives(entry: string, jdk: string, packaged: boolean, ho merged = result; stdio[1] = stdio[2] = merged.writer; } else if (redirectError) stdio[2] = stdio[1]; - const child = spawn(command, args, { cwd: dir ? decode(dir) : t.vm.opts.cwd, env, stdio: stdio as [any, any, any] }); + const cwd = dir ? decode(dir) : t.vm.opts.cwd; + const program = java ? command : findProgram(command, cwd ?? process.cwd(), (t.vm.opts.env ?? process.env).PATH); + if (typeof program !== "string") { + throw new PendingThrow("java/io/IOException", program.code === "EACCES" ? "error=13, Permission denied" : "error=2, No such file or directory"); + } + const child = spawn(program, args, { cwd, env, stdio: stdio as [any, any, any] }); merged?.writer.destroy(); const status = (code: number | null, signal: NodeJS.Signals | null) => code ?? 128 + ((os.constants.signals as Record)[signal ?? ""] ?? 0); diff --git a/test/fs.test.ts b/test/fs.test.ts index 5798824..238071d 100644 --- a/test/fs.test.ts +++ b/test/fs.test.ts @@ -38,3 +38,14 @@ test("host path resolution leaves other mounts and memory paths lexical", () => memory.mount("/files", new MemoryBackend()); assert.equal(memory.resolvePath("/files/dir/../file"), "/files/file"); }); + +test("realpath keeps the mount prefix of a mount point", () => { + const fs = new VFS(); + const withRealpath = () => Object.assign(new MemoryBackend(), { realpath: (p: string) => p }); + fs.mount("/", withRealpath()); + fs.mount("/jdk", withRealpath()); + assert.equal(fs.realpath("/jdk"), "/jdk"); + assert.equal(fs.realpath("/jdk/lib"), "/jdk/lib"); + assert.equal(fs.realpath("/"), "/"); + assert.equal(fs.realpath("/dir/file"), "/dir/file"); +}); diff --git a/test/java/CreateModes.java b/test/java/CreateModes.java new file mode 100644 index 0000000..266c062 --- /dev/null +++ b/test/java/CreateModes.java @@ -0,0 +1,52 @@ +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; + +import static java.nio.file.StandardOpenOption.*; + +public class CreateModes { + static void show(String what, Path p) throws IOException { + System.out.println(what + " " + PosixFilePermissions.toString(Files.getPosixFilePermissions(p))); + } + + static FileAttribute> perms(String s) { + return PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(s)); + } + + public static void main(String[] args) throws IOException { + Path dir = Files.createTempDirectory("modes"); + try { + Path a = Files.createFile(dir.resolve("a"), perms("rw-------")); + show("createFile 600", a); + Path b = Files.createFile(dir.resolve("b"), perms("rwxr-x---")); + show("createFile 750", b); + Path c = Files.createFile(dir.resolve("c")); + show("createFile default", c); + Path d = Files.createDirectory(dir.resolve("d"), perms("rwx------")); + show("createDirectory 700", d); + Path t = Files.createTempFile(dir, "t", ".tmp"); + show("createTempFile", t); + Path td = Files.createTempDirectory(dir, "td"); + show("createTempDirectory", td); + // a read-only file can still be written through the descriptor that created it + Path f = dir.resolve("f"); + try (FileChannel ch = FileChannel.open(f, Set.of(CREATE_NEW, WRITE), perms("r--r--r--"))) { + ch.write(ByteBuffer.wrap("data".getBytes())); + } + show("channel 444", f); + System.out.println(Files.readString(f)); + Files.setPosixFilePermissions(f, PosixFilePermissions.fromString("rw-------")); + Files.delete(f); + } finally { + try (var s = Files.walk(dir)) { + s.sorted(java.util.Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); + } + } + } +} diff --git a/test/java/OpenFileDescriptors.java b/test/java/OpenFileDescriptors.java new file mode 100644 index 0000000..638d24f --- /dev/null +++ b/test/java/OpenFileDescriptors.java @@ -0,0 +1,26 @@ +import com.sun.management.UnixOperatingSystemMXBean; +import java.io.File; +import java.io.FileInputStream; +import java.io.RandomAccessFile; +import java.lang.management.ManagementFactory; + +public class OpenFileDescriptors { + public static void main(String[] args) throws Exception { + var os = (UnixOperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + System.out.println("max > 0: " + (os.getMaxFileDescriptorCount() > 0)); + System.out.println("open >= 3: " + (os.getOpenFileDescriptorCount() >= 3)); + File f = File.createTempFile("fds", null); + try { + long before = os.getOpenFileDescriptorCount(); + var in = new FileInputStream(f); + var raf = new RandomAccessFile(f, "rw"); + System.out.println("opened 2: " + (os.getOpenFileDescriptorCount() - before)); + in.close(); + System.out.println("closed 1: " + (os.getOpenFileDescriptorCount() - before)); + raf.close(); + System.out.println("closed 2: " + (os.getOpenFileDescriptorCount() - before)); + } finally { + f.delete(); + } + } +} diff --git a/test/java/ProcessLookup.java b/test/java/ProcessLookup.java new file mode 100644 index 0000000..6705102 --- /dev/null +++ b/test/java/ProcessLookup.java @@ -0,0 +1,37 @@ +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class ProcessLookup { + static void start(String what, String... command) { + try { + Process p = new ProcessBuilder(command).start(); + System.out.println(what + " exit " + p.waitFor()); + } catch (IOException e) { + // HotSpot's launch helper words the reason differently from a direct fork + var m = java.util.regex.Pattern.compile("error[=:] ?(\\d+)").matcher(e.getCause().getMessage()); + System.out.println(what + " errno " + (m.find() ? m.group(1) : e.getCause().getMessage())); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) throws Exception { + File dir = Files.createTempDirectory("lookup").toFile(); + try { + start("empty", ""); + start("missing", "no-such-program-xyz"); + start("missing path", dir + "/no-such-program"); + start("directory", dir.getPath()); + File script = new File(dir, "plain"); + Files.writeString(script.toPath(), "#!/bin/true\n"); + start("not executable", script.getPath()); + script.setExecutable(true); + start("script", script.getPath()); + start("on PATH", "true"); + } finally { + for (File f : dir.listFiles()) f.delete(); + dir.delete(); + } + } +} diff --git a/test/java/SecureDirectories.java b/test/java/SecureDirectories.java new file mode 100644 index 0000000..13df5df --- /dev/null +++ b/test/java/SecureDirectories.java @@ -0,0 +1,98 @@ +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.SecureDirectoryStream; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; + +public class SecureDirectories { + public static void main(String[] args) throws IOException { + Path dir = Files.createTempDirectory("secure"); + Path other = Files.createTempDirectory("secure"); + try { + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + var secure = (SecureDirectoryStream) stream; + try (SeekableByteChannel ch = secure.newByteChannel(Path.of("a"), + Set.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))) { + ch.write(ByteBuffer.wrap("hello".getBytes())); + } + System.out.println("created " + Files.readString(dir.resolve("a"))); + System.out.println("is file " + secure.getFileAttributeView(Path.of("a"), BasicFileAttributeView.class) + .readAttributes().isRegularFile()); + try (SecureDirectoryStream sub = secure.newDirectoryStream(Path.of("."))) { + var names = new TreeSet(); + for (Path p : sub) names.add(p.getFileName().toString()); + System.out.println("listed " + names); + } + try (var notDir = secure.newDirectoryStream(Path.of("a"))) { + System.out.println("opened a file as a directory"); + } catch (NotDirectoryException e) { + System.out.println("not a directory"); + } + try (DirectoryStream target = Files.newDirectoryStream(other)) { + secure.move(Path.of("a"), (SecureDirectoryStream) target, Path.of("b")); + } + System.out.println("moved " + Files.exists(dir.resolve("a")) + " " + Files.exists(other.resolve("b"))); + try { + secure.deleteDirectory(Path.of("missing-dir")); + } catch (java.nio.file.NoSuchFileException e) { + System.out.println("no such file"); + } + for (String name : new String[] {".", ".."}) { + try { + secure.deleteDirectory(Path.of(name)); + System.out.println("deleted directory " + name); + } catch (IOException e) { + System.out.println("delete directory " + name + " " + e.getClass().getSimpleName()); + } + try { + secure.deleteFile(Path.of(name)); + System.out.println("deleted file " + name); + } catch (IOException e) { + System.out.println("delete file " + name + " " + e.getClass().getSimpleName()); + } + } + System.out.println("directory kept " + Files.isDirectory(dir)); + } + try (DirectoryStream stream = Files.newDirectoryStream(other)) { + var secure = (SecureDirectoryStream) stream; + secure.deleteFile(Path.of("b")); + System.out.println("deleted " + Files.exists(other.resolve("b"))); + } + Path gone = Files.createTempDirectory("secure"); + try (DirectoryStream stream = Files.newDirectoryStream(gone)) { + var secure = (SecureDirectoryStream) stream; + Files.delete(gone); + try (var ch = secure.newByteChannel(Path.of("late"), + Set.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))) { + System.out.println("created in a deleted directory"); + } catch (IOException e) { + System.out.println("deleted directory " + e.getClass().getSimpleName()); + } + try { + secure.deleteFile(Path.of("late")); + } catch (IOException e) { + System.out.println("delete in deleted directory " + e.getClass().getSimpleName()); + } + } + try { + Files.newDirectoryStream(other.resolve("nothing")); + } catch (java.nio.file.NoSuchFileException e) { + System.out.println("missing directory"); + } + } finally { + for (Path d : new Path[] {dir, other}) { + try (var s = Files.walk(d)) { + s.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); + } + } + } + } +} diff --git a/test/java/SymbolicLinks.java b/test/java/SymbolicLinks.java new file mode 100644 index 0000000..4c77797 --- /dev/null +++ b/test/java/SymbolicLinks.java @@ -0,0 +1,81 @@ +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.stream.Collectors; + +public class SymbolicLinks { + public static void main(String[] args) throws IOException { + Path dir = Files.createTempDirectory("links").toRealPath(); + try { + Path file = Files.writeString(dir.resolve("file"), "data"); + Path sub = Files.createDirectory(dir.resolve("sub")); + Path toFile = Files.createSymbolicLink(dir.resolve("toFile"), Path.of("file")); + Path toDir = Files.createSymbolicLink(dir.resolve("toDir"), sub); + Path dangling = Files.createSymbolicLink(dir.resolve("dangling"), Path.of("missing")); + + System.out.println("readlink " + Files.readSymbolicLink(toFile) + " " + (Files.readSymbolicLink(toDir).equals(sub))); + System.out.println("isSymbolicLink " + Files.isSymbolicLink(toFile) + " " + Files.isSymbolicLink(file)); + System.out.println("read through link " + Files.readString(toFile)); + System.out.println("exists " + Files.exists(toFile) + " " + Files.exists(dangling) + " " + Files.exists(dangling, LinkOption.NOFOLLOW_LINKS)); + System.out.println("isDirectory " + Files.isDirectory(toDir) + " " + Files.isDirectory(toDir, LinkOption.NOFOLLOW_LINKS)); + System.out.println("size nofollow " + Files.readAttributes(toFile, java.nio.file.attribute.BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).size()); + System.out.println("toRealPath " + dir.relativize(toFile.toRealPath()) + " " + dir.relativize(toDir.toRealPath())); + System.out.println("canonical " + new File(toFile.toString()).getCanonicalPath().equals(file.toString())); + System.out.println("canonical missing " + new File(dir + "/toDir/new").getCanonicalPath().equals(sub + "/new")); + + Files.writeString(toDir.resolve("inner"), "x"); + try (var list = Files.list(toDir)) { + System.out.println("list through link " + list.map(p -> p.getFileName().toString()).collect(Collectors.toList())); + } + + try { + Files.createSymbolicLink(toFile, file); + } catch (java.nio.file.FileAlreadyExistsException e) { + System.out.println("exists error"); + } + try { + Files.readSymbolicLink(file); + } catch (java.nio.file.NotLinkException e) { + System.out.println("not a link"); + } + try { + Files.createDirectory(dangling); + } catch (java.nio.file.FileAlreadyExistsException e) { + System.out.println("directory over dangling link"); + } + for (String name : new String[] {"sub/.", "sub/.."}) { + try { + Files.delete(dir.resolve(name)); + System.out.println("deleted " + name); + } catch (IOException e) { + System.out.println("delete " + name + " " + e.getClass().getSimpleName()); + } + } + System.out.println("sub kept " + Files.isDirectory(sub)); + + Path copy = Files.copy(toFile, dir.resolve("copy"), LinkOption.NOFOLLOW_LINKS); + System.out.println("copy of link " + Files.isSymbolicLink(copy)); + Path moved = Files.move(toDir, dir.resolve("moved"), StandardCopyOption.ATOMIC_MOVE); + System.out.println("moved link " + Files.isSymbolicLink(moved)); + + Files.delete(moved); + Files.delete(toFile); + System.out.println("target kept " + Files.exists(sub) + " " + Files.exists(file)); + System.out.println("deleting dangling " + Files.deleteIfExists(dangling)); + try { + Files.delete(dangling); + } catch (NoSuchFileException e) { + System.out.println("already gone"); + } + } finally { + try (var s = Files.walk(dir)) { + s.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); + } + } + } +} diff --git a/test/java/VmOptions.java b/test/java/VmOptions.java new file mode 100644 index 0000000..3669ad0 --- /dev/null +++ b/test/java/VmOptions.java @@ -0,0 +1,17 @@ +import com.sun.management.HotSpotDiagnosticMXBean; +import java.lang.management.ManagementFactory; + +public class VmOptions { + public static void main(String[] args) { + var diag = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + for (String name : new String[] {"LockingMode", "HeapDumpOnOutOfMemoryError"}) { + var o = diag.getVMOption(name); + System.out.println(o.getName() + "=" + o.getValue() + " " + o.getOrigin() + " writeable=" + o.isWriteable()); + } + try { + diag.getVMOption("NoSuchOption"); + } catch (IllegalArgumentException e) { + System.out.println("unknown: " + e.getMessage()); + } + } +} diff --git a/test/jtreg/passing.txt b/test/jtreg/passing.txt index 2debf74..236aeb8 100644 --- a/test/jtreg/passing.txt +++ b/test/jtreg/passing.txt @@ -408,8 +408,10 @@ java/io/File/CreateNewFile.java java/io/File/DeleteOnExit.java java/io/File/DeleteOnExitLong.java java/io/File/DeleteOnExitNPE.java +java/io/File/EmptyPath.java java/io/File/FileMethods.java java/io/File/GetAbsolutePath.java +java/io/File/GetCanonicalPath.java java/io/File/GetParent.java java/io/File/HashCodeEquals.java java/io/File/IsAbsolute.java @@ -445,8 +447,11 @@ java/io/FileInputStream/OpenDir.java java/io/FileInputStream/OpsAfterClose.java java/io/FileInputStream/ReadXBytes.java java/io/FileInputStream/TransferTo.java +java/io/FileInputStream/UnreferencedFISClosesFd.java +java/io/FileOutputStream/AtomicAppend.java java/io/FileOutputStream/ManyFiles.java java/io/FileOutputStream/OpsAfterClose.java +java/io/FileOutputStream/UnreferencedFOSClosesFd.java java/io/FilePermission/Correctness.java java/io/FilePermission/FilePermissionCollection.java java/io/FilePermission/FilePermissionCollectionMerge.java @@ -542,6 +547,7 @@ java/io/RandomAccessFile/ReadLine.java java/io/RandomAccessFile/ReadWritePrimitives.java java/io/RandomAccessFile/Seek.java java/io/RandomAccessFile/SetLength.java +java/io/RandomAccessFile/UnreferencedRAFClosesFd.java java/io/RandomAccessFile/WriteBytesChars.java java/io/RandomAccessFile/WriteUTF.java java/io/RandomAccessFile/skipBytes/SkipBytes.java @@ -854,23 +860,39 @@ java/lang/Package/PackageVersionTest.java java/lang/PrimitiveSumMinMaxTest.java java/lang/Process/WaitFor.java java/lang/Process/WaitForDuration.java +java/lang/ProcessBuilder/CloseRace.java java/lang/ProcessBuilder/DestroyTest.java java/lang/ProcessBuilder/InheritIOEHandle.java +java/lang/ProcessBuilder/InheritIOTest.java +java/lang/ProcessBuilder/NonPipelineLeaksFD.java +java/lang/ProcessBuilder/NonPipelineLeaksFD.java#2 java/lang/ProcessBuilder/PipelineTest.java java/lang/ProcessBuilder/ProcessReaperCCL.java +java/lang/ProcessBuilder/ReaderWriterTest.java +java/lang/ProcessBuilder/UnblockSignals.java +java/lang/ProcessBuilder/UnblockSignals.java#2 java/lang/ProcessBuilder/Zombies.java java/lang/ProcessHandle/Basic.java +java/lang/ProcessHandle/InfoTest.java +java/lang/ProcessHandle/OnExitTest.java +java/lang/ProcessHandle/TreeTest.java java/lang/RuntimePermission/ExitVM.java java/lang/RuntimeTests/Resources.java java/lang/RuntimeTests/Version/Basic.java java/lang/RuntimeTests/Version/VersionProps.java +java/lang/RuntimeTests/exec/ArgWithSpaceAndFinalBackslash.java java/lang/RuntimeTests/exec/BadEnvp.java java/lang/RuntimeTests/exec/ConcurrentRead.java +java/lang/RuntimeTests/exec/Duped.java java/lang/RuntimeTests/exec/ExecEmptyString.java java/lang/RuntimeTests/exec/ExecWithInput.java +java/lang/RuntimeTests/exec/ExecWithLotsOfArgs.java +java/lang/RuntimeTests/exec/ExitValue.java java/lang/RuntimeTests/exec/LotsOfDestroys.java java/lang/RuntimeTests/exec/LotsOfOutput.java +java/lang/RuntimeTests/exec/SetCwd.java java/lang/RuntimeTests/exec/SleepyCat.java +java/lang/RuntimeTests/exec/Status.java java/lang/RuntimeTests/exec/WinCommand.java java/lang/RuntimeTests/loadLibrary/LoadLibraryTest.java java/lang/RuntimeTests/shutdown/Basic.java @@ -1060,19 +1082,31 @@ java/lang/Thread/SleepWithDuration.java java/lang/Thread/ThreadStateTest.java java/lang/Thread/ThreadStopTest.java java/lang/Thread/UncaughtExceptionsTest.java +java/lang/Thread/virtual/BigStackChunk.java java/lang/Thread/virtual/Collectable.java +java/lang/Thread/virtual/CustomScheduler.java java/lang/Thread/virtual/Locking.java +java/lang/Thread/virtual/MonitorEnterWaitOOME.java#2 +java/lang/Thread/virtual/ParkWithFixedThreadPool.java java/lang/Thread/virtual/Reflection.java java/lang/Thread/virtual/ShutdownHook.java java/lang/Thread/virtual/ThreadLocals.java +java/lang/Thread/virtual/TraceVirtualThreadLocals.java +java/lang/Thread/virtual/YieldQueuing.java +java/lang/Thread/virtual/stress/CompletableFutureTimedGet.java +java/lang/Thread/virtual/stress/GetStackTraceALotWhenParking.java java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java#2 java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java#3 java/lang/Thread/virtual/stress/NotifiedThenTimedOutWait.java#2 +java/lang/Thread/virtual/stress/PingPong.java +java/lang/Thread/virtual/stress/PingPong.java#2 +java/lang/Thread/virtual/stress/SleepALot.java java/lang/Thread/virtual/stress/TimedWaitALot.java java/lang/Thread/virtual/stress/TimedWaitALot.java#2 java/lang/Thread/virtual/stress/TimedWaitALot.java#3 java/lang/Thread/virtual/stress/TimedWaitALot.java#4 +java/lang/Thread/virtual/stress/YieldALot.java java/lang/ThreadGroup/BasicTests.java java/lang/ThreadLocal/Basic.java java/lang/ThreadLocal/ImmutableLocal.java @@ -1968,6 +2002,7 @@ java/nio/channels/DatagramChannel/Truncate.java java/nio/channels/DatagramChannel/UseDGWithIPv6.java java/nio/channels/FileChannel/Args.java java/nio/channels/FileChannel/AtomicAppend.java +java/nio/channels/FileChannel/CleanerTest.java java/nio/channels/FileChannel/CloseDuringTransfer.java java/nio/channels/FileChannel/ClosedByInterrupt.java java/nio/channels/FileChannel/ClosedChannelTransfer.java @@ -2132,11 +2167,15 @@ java/nio/charset/coders/IsLegalReplacement.java java/nio/charset/coders/StreamTimeout.java java/nio/charset/coders/Surrogates.java java/nio/file/DirectoryStream/Basic.java +java/nio/file/DirectoryStream/SecureDS.java java/nio/file/FileStore/Basic.java java/nio/file/FileSystem/Basic.java java/nio/file/Files/BytesAndLines.java java/nio/file/Files/CallWithInterruptSet.java +java/nio/file/Files/CopyAndMove.java java/nio/file/Files/CopyInterference.java +java/nio/file/Files/CopyMoveVariations.java +java/nio/file/Files/CopyProcFile.java java/nio/file/Files/CopyToNonDefaultFS.java java/nio/file/Files/CreateDirectories.java java/nio/file/Files/CustomOptions.java @@ -2147,9 +2186,14 @@ java/nio/file/Files/Links.java java/nio/file/Files/Misc.java java/nio/file/Files/Mismatch.java java/nio/file/Files/NameLimits.java +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/TemporaryFiles.java java/nio/file/Files/probeContentType/ForceLoad.java +java/nio/file/Files/probeContentType/ParallelProbes.java +java/nio/file/Files/walkFileTree/FindTest.java java/nio/file/Files/walkFileTree/MaxDepth.java java/nio/file/Files/walkFileTree/Nulls.java java/nio/file/Files/walkFileTree/SkipSiblings.java @@ -2172,6 +2216,7 @@ java/nio/file/attribute/AclFileAttributeView/Basic.java java/nio/file/attribute/BasicFileAttributeView/Basic.java java/nio/file/attribute/DosFileAttributeView/Basic.java java/nio/file/attribute/FileTime/Basic.java +java/nio/file/attribute/PosixFileAttributeView/Basic.java java/nio/file/attribute/UserDefinedFileAttributeView/Basic.java java/nio/file/etc/Exceptions.java java/security/AccessControlContext/CheckCtor.java @@ -3506,6 +3551,7 @@ jdk/nio/zipfs/PathOps.java jdk/nio/zipfs/PropertyPermissionTests.java jdk/nio/zipfs/ReleaseDeflater.java jdk/nio/zipfs/TestLocOffsetFromZip64EF.java +jdk/nio/zipfs/TestPosix.java jdk/nio/zipfs/UpdateEntryTest.java jdk/nio/zipfs/ZFSTests.java jdk/nio/zipfs/ZeroDate.java diff --git a/test/jtreg/times.tsv b/test/jtreg/times.tsv index 169cc3d..982a3b8 100644 --- a/test/jtreg/times.tsv +++ b/test/jtreg/times.tsv @@ -40,6 +40,7 @@ 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 59689 java/security/cert/CertPathValidator/OCSP/OCSPTimeout.java 59400 java/time/test/java/time/chrono/TestThaiBuddhistChronoImpl.java @@ -147,6 +148,7 @@ 26649 java/lang/Thread/virtual/stress/NotifiedThenTimedOutWait.java#2 26478 java/nio/channels/Selector/ChangingInterests.java 26329 java/text/Format/DateFormat/SimpleDateFormatPatternTest.java +26157 java/lang/ProcessHandle/OnExitTest.java 25808 java/lang/invoke/ClassValueTest.java 25743 java/util/zip/DeflateIn_InflateOut.java 25299 java/net/httpclient/websocket/PendingBinaryPongClose.java @@ -186,6 +188,7 @@ 20178 java/security/KeyAgreement/KeyAgreementTest.java 20120 java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java 19836 java/net/httpclient/ssltest/TlsVersionTest.java +19801 java/lang/ProcessHandle/TreeTest.java 19748 java/lang/String/HashCode.java 19622 java/lang/invoke/LFCaching/LFSingleThreadCachingTest.java 19399 java/util/Arrays/SortingNearlySortedPrimitive.java @@ -242,7 +245,9 @@ 16708 java/net/Authenticator/B4769350.java 16651 java/text/Format/common/FormatIteratorTest.java 16608 java/util/concurrent/ThreadPerTaskExecutor/ThreadPerTaskExecutorTest.java#2 +16579 java/io/FileOutputStream/UnreferencedFOSClosesFd.java 16505 java/lang/String/CompactString/EqualsIgnoreCase.java +16499 java/io/FileInputStream/UnreferencedFISClosesFd.java 16471 java/text/Format/DateFormat/IntlTestDateFormat.java 16468 java/net/PortUnreachableException/OneExceptionOnly.java 16420 java/nio/channels/Selector/SelectWithConsumer.java @@ -264,6 +269,7 @@ 15637 java/lang/module/ConfigurationTest.java 15622 java/lang/String/CompactString/Contains.java 15589 java/net/httpclient/websocket/BlowupOutputQueue.java +15528 java/lang/Thread/virtual/stress/PingPong.java 15519 java/net/DatagramSocketImpl/CheckDiscard.java 15510 java/time/test/java/time/TestOffsetDateTime_instants.java 15430 java/net/httpclient/DebugLoggerTest.java @@ -274,6 +280,7 @@ 15296 java/util/Calendar/CalendarRegression.java 15288 java/lang/reflect/callerCache/CustomLoaderTest.java 15259 java/util/LinkedHashSet/Basic.java +15112 java/lang/Thread/virtual/stress/GetStackTraceALotWhenParking.java 15079 java/security/KeyAgreement/NegativeTest.java 15076 java/io/Serializable/serialFilter/MixedFiltersTest.java 15072 java/util/concurrent/atomic/DoubleAdderDemo.java @@ -282,6 +289,7 @@ 14999 java/util/concurrent/FutureTask/DoneMeansDone.java 14964 java/util/HexFormat/HexFormatTest.java 14914 java/net/Authenticator/Deadlock.java +14876 java/lang/Thread/virtual/stress/PingPong.java#2 14806 java/nio/channels/Selector/LotsOfInterrupts.java 14792 java/lang/String/CompactString/CodePointBefore.java 14787 java/lang/reflect/Proxy/ProxyLayerTest.java @@ -692,6 +700,7 @@ 7920 java/util/Calendar/CalendarDataTest.java 7912 java/lang/Thread/ThreadStopTest.java 7907 java/math/BigDecimal/StrippingZerosTest.java +7901 java/lang/ProcessBuilder/ReaderWriterTest.java 7897 java/lang/RuntimeTests/shutdown/ShutdownHooks.java 7885 java/lang/String/CompactString/CodePointAt.java 7881 java/util/Calendar/bug4028518.java @@ -722,6 +731,7 @@ 7724 java/net/ServerSocket/ClosedServerSocketTest.java 7719 java/text/Format/DateFormat/Bug8193444.java 7718 java/time/tck/java/time/chrono/serial/TCKChronoZonedDateTimeSerialization.java +7713 jdk/nio/zipfs/TestPosix.java 7706 java/text/Format/DecimalFormat/SettersShouldThrowNPETest.java 7703 java/security/Provider/NewInstance.java 7699 java/util/zip/ZipFile/Assortment.java @@ -1184,6 +1194,7 @@ 5424 java/beans/XMLEncoder/Test5023559.java 5420 java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevelRevoked.java 5417 java/util/Locale/CaseCheckVariant.java +5410 java/lang/ProcessBuilder/InheritIOTest.java 5409 java/lang/management/ManagementFactory/ProxyExceptions.java 5391 java/util/zip/ZipFile/InvalidCompressedSizeTest.java 5389 java/lang/String/ToLowerCase.java @@ -1217,6 +1228,7 @@ 5300 java/net/httpclient/LineStreamsAndSurrogatesTest.java 5298 java/math/BigDecimal/IntegralDivisionTests.java 5296 java/text/Format/MessageFormat/ListSubFormats.java +5291 java/nio/file/Files/CopyMoveVariations.java 5289 jdk/nio/zipfs/Basic.java 5288 java/time/test/java/time/format/ZoneName.java 5281 java/nio/file/Files/CallWithInterruptSet.java @@ -1265,6 +1277,7 @@ 5110 java/net/Socket/TestClose.java 5110 java/beans/XMLEncoder/javax_swing_border_BevelBorder.java 5089 java/lang/reflect/ChainedReflection.java +5088 java/lang/Thread/virtual/MonitorEnterWaitOOME.java#2 5085 java/lang/StackWalker/GetCallerClassTest.java 5078 javax/xml/jaxp/testng/parse/jdk7156085/UTF8ReaderBug.java 5077 java/io/File/LastModifiedTest.java @@ -1290,6 +1303,7 @@ 4979 java/util/Locale/ProviderPoolMultiThreadAccess.java 4979 java/lang/Thread/virtual/ThreadLocals.java 4977 java/time/test/java/time/TestDuration.java +4969 java/lang/ProcessHandle/InfoTest.java 4967 java/io/Reader/Skip.java 4966 java/io/Reader/NullReader.java 4962 java/text/Collator/SurrogatesTest.java @@ -1335,6 +1349,7 @@ 4813 java/io/PrintStream/OversynchronizedTest.java 4810 java/security/cert/CertPathBuilder/zeroLengthPath/ZeroLengthPath.java 4803 java/time/tck/java/time/zone/TCKZoneOffsetTransition.java +4803 java/lang/Thread/virtual/stress/YieldALot.java 4794 java/util/logging/LogManager/Configuration/updateConfiguration/HandlersOnComplexUpdate.java 4794 java/nio/channels/SelectionKey/AtomicUpdates.java 4794 java/net/DatagramSocket/Send12k.java @@ -1343,6 +1358,7 @@ 4787 java/security/Security/Nulls.java 4787 java/nio/channels/SocketChannel/Write.java 4781 java/util/Locale/ISO3166.java +4781 java/lang/Thread/virtual/TraceVirtualThreadLocals.java 4776 java/beans/Introspector/8130937/TestBooleanBeanProperties.java 4774 java/math/BigDecimal/AddTests.java 4772 java/util/logging/LogRecordThreadIdTest.java @@ -1705,6 +1721,7 @@ 3814 java/lang/invoke/lambda/MetafactoryArityTest.java 3813 java/time/tck/java/time/serial/TCKYearSerialization.java 3811 java/util/concurrent/CountDownLatch/Basic.java +3810 java/io/File/EmptyPath.java 3808 java/net/InetAddress/CachedUnknownHostName.java 3801 java/util/jar/JarFile/TurkCert.java 3797 java/security/Security/SynchronizedAccess.java @@ -1964,6 +1981,7 @@ 3439 java/beans/PropertyChangeSupport/TestSerialization.java 3437 java/math/BigDecimal/DivideTests.java 3436 javax/xml/jaxp/datatype/8033980/CalendarDuration1097Test.java +3436 java/lang/ProcessBuilder/CloseRace.java 3433 java/awt/geom/AffineTransform/EqualsAndHashCode.java 3431 java/net/InetAddress/B6246242.java 3431 java/awt/geom/Area/TightBBox.java @@ -1975,6 +1993,7 @@ 3422 java/io/DataOutputStream/CounterOverflow.java 3421 java/net/httpclient/LineSubscribersAndSurrogatesTest.java 3421 java/lang/reflect/Proxy/nonJavaNames/Test.java +3420 java/lang/Thread/virtual/CustomScheduler.java 3417 java/beans/Introspector/Test6447751.java 3415 java/util/Collections/CheckedQueue.java 3415 java/net/URLConnection/UnknownContentType.java @@ -2022,6 +2041,7 @@ 3368 java/net/NetworkInterface/NetParamsTest.java 3368 java/lang/ref/EarlyTimeout.java 3367 java/security/cert/CertPath/Serialize.java +3367 java/lang/RuntimeTests/exec/SetCwd.java 3365 java/util/logging/GetGlobalTest.java 3363 java/math/BigDecimal/CompareToTests.java 3361 java/util/AbstractMap/AbstractMapClone.java @@ -2052,6 +2072,7 @@ 3337 java/net/InterfaceAddress/Equals.java 3337 java/beans/XMLEncoder/6777487/TestCheckedCollection.java 3336 java/lang/String/IsEmpty.java +3335 java/io/File/GetCanonicalPath.java 3334 java/beans/Introspector/Test4168833.java 3333 java/util/concurrent/BlockingQueue/Interrupt.java 3333 java/io/RandomAccessFile/FileLengthTest.java @@ -2086,6 +2107,7 @@ 3298 java/awt/geom/Arc2D/SerializationTest.java 3297 java/util/Collections/CheckedIdentityMap.java 3297 java/beans/beancontext/Test4132698.java +3294 java/nio/file/Files/NotADirectory.java 3291 java/lang/RuntimeTests/Resources.java 3288 java/nio/channels/Pipe/PipeChannel.java 3288 java/io/File/ListNull.java @@ -2123,6 +2145,7 @@ 3243 java/text/Format/DecimalFormat/TieRoundingTest.java 3242 java/util/logging/LoggerSupplierAPIsTest.java 3240 java/nio/file/FileSystem/Basic.java +3240 java/lang/ProcessBuilder/NonPipelineLeaksFD.java 3235 java/lang/Math/RoundTests.java 3234 java/util/LinkedHashMap/EmptyMapIterator.java 3233 java/net/URLClassLoader/B7050028.java @@ -2189,6 +2212,7 @@ 3166 java/beans/VetoableChangeSupport/TestListeners.java 3161 java/lang/ClassLoader/BadRegisterAsParallelCapableCaller.java 3160 java/nio/channels/SelectionKey/RacyRegister.java +3158 java/lang/Thread/virtual/stress/CompletableFutureTimedGet.java 3156 java/io/Serializable/proxy/Basic.java 3156 java/beans/PropertyEditor/TestLongTypeValue.java 3155 java/lang/management/CompositeData/MemoryUsageCompositeData.java @@ -2208,6 +2232,7 @@ 3127 java/beans/XMLDecoder/spec/TestNull.java 3124 java/security/KeyStore/PKCS12/CheckDefaults.java 3123 java/nio/charset/coders/Errors.java +3123 java/lang/ProcessBuilder/NonPipelineLeaksFD.java#2 3122 java/util/logging/IsLoggableHandlerTest.java 3121 javax/crypto/JceSecurity/FinalRestricted.java 3121 java/util/Properties/LoadAndStoreNPE.java @@ -2299,6 +2324,7 @@ 2977 java/security/KeyFactory/Failover.java 2976 java/security/cert/X509Certificate/X509BadCertificate.java 2975 java/text/AttributedCharacterIterator/Attribute/ReadResolve.java +2975 java/lang/RuntimeTests/exec/ExecWithLotsOfArgs.java 2974 java/util/Date/TZ.java 2972 java/text/Collator/CurrencyCollate.java 2971 java/security/cert/CertPathBuilder/targetConstraints/BuildEEBasicConstraints.java @@ -2443,6 +2469,7 @@ 2817 java/net/HttpURLConnection/getResponseCode.java 2816 java/util/HashMap/PutNullKey.java 2816 java/lang/Double/BitwiseConversion.java +2816 java/lang/ProcessBuilder/UnblockSignals.java#2 2815 java/nio/channels/ServerSocketChannel/Basic.java 2815 java/io/Serializable/enum/unshared/Test.java 2815 java/io/charStreams/UTF8.java @@ -2559,6 +2586,7 @@ 2677 java/net/NetworkInterface/NetworkInterfaceRetrievalTests.java 2676 java/io/Serializable/auditStreamSubclass/AuditStreamSubclass.java 2675 java/lang/reflect/TypeVariable/TestAnnotatedElement.java +2675 java/lang/RuntimeTests/exec/Status.java 2674 java/lang/ref/ReferenceEnqueuePending.java 2674 java/lang/reflect/Parameter/GetAnnotatedTypeTest.java 2670 java/io/Serializable/partialClassDesc/PartialClassDesc.java @@ -2614,6 +2642,7 @@ 2623 java/lang/constant/DynamicConstantDescTest.java 2622 java/lang/Thread/MainThreadTest.java 2621 java/util/jar/JarFile/Constructor.java +2621 java/lang/ProcessBuilder/UnblockSignals.java 2620 java/io/pathNames/win32/BadDriveLetter.java 2615 java/io/Serializable/enum/ignoreSerializationFields/Test.java 2612 java/util/ArrayList/IteratorMicroBenchmark.java @@ -2669,6 +2698,7 @@ 2554 java/text/Format/DateFormat/Bug4396385.java 2554 java/lang/management/ThreadMXBean/ThreadCounts.java 2552 java/net/InetSocketAddress/CreateUnresolved.java +2551 java/nio/file/Files/CopyAndMove.java 2550 java/util/logging/RootLogger/RootLevelInConfigFile.java 2550 java/nio/channels/FileChannel/TransferOverlappedFileChannel.java 2550 java/net/URL/B6529759.java @@ -2682,6 +2712,7 @@ 2541 javax/xml/jaxp/transform/8004476/XSLTExFuncTest.java 2541 java/lang/reflect/classInitialization/ExceptionInClassInitialization.java 2538 java/util/PropertyPermission/PropertyPermissionCollection.java +2538 java/lang/Thread/virtual/YieldQueuing.java 2534 java/util/TreeMap/EmptyMapAndNulls.java 2528 java/io/charStreams/BufferedReaderMark.java 2527 java/io/Serializable/oldTests/AnnotateClass.java @@ -2696,6 +2727,7 @@ 2521 java/text/Format/DateFormat/Bug6609750.java 2521 java/io/BufferedReader/BigMark.java 2519 java/util/AbstractSequentialList/AddAll.java +2518 java/lang/RuntimeTests/exec/ExitValue.java 2517 java/beans/Introspector/Test6868189.java 2513 java/security/Provider/GetServiceRace.java 2512 java/nio/channels/DatagramChannel/Disconnect.java @@ -2754,6 +2786,7 @@ 2448 java/text/Bidi/BidiEmbeddingTest.java 2448 java/math/BigInteger/DoubleValueOverflow.java 2447 java/lang/Character/TestUndefinedIgnorable.java +2446 java/lang/RuntimeTests/exec/ArgWithSpaceAndFinalBackslash.java 2445 java/lang/Class/getDeclaredField/ClassDeclaredFieldsTest.java 2441 java/util/logging/HigherResolutionTimeStamps/SerializeLogRecord.java 2439 java/nio/charset/CharsetEncoder/CanEncode.java @@ -2836,6 +2869,7 @@ 2365 java/lang/reflect/Proxy/CharType.java 2365 java/lang/Boolean/GetBoolean.java 2365 java/io/Serializable/verifyDynamicObjHandleTable/VerifyDynamicObjHandleTable.java +2364 java/lang/Thread/virtual/BigStackChunk.java 2363 java/util/zip/GZIP/Accordion.java 2362 java/util/ResourceBundle/Control/BadControlsTest.java 2362 java/lang/reflect/Generics/getAnnotationTest.java @@ -2977,6 +3011,7 @@ 2208 java/lang/StrictMath/ExactArithTests.java 2207 java/util/Collections/RotateEmpty.java 2206 java/lang/reflect/Generics/TestMissingTypeVariable.java +2206 java/io/RandomAccessFile/UnreferencedRAFClosesFd.java 2204 java/lang/Class/getClasses/Sanity.java 2203 java/util/Formatter/Constructors.java 2203 java/io/RandomAccessFile/EOF.java @@ -3033,6 +3068,7 @@ 2125 java/lang/String/ICCBasher.java 2124 java/lang/ThreadLocal/Basic.java 2123 java/lang/StringBuffer/Capacity.java +2123 java/nio/file/Files/CopyProcFile.java 2120 java/io/LineNumberReader/ReadReadLine.java 2118 java/io/Serializable/finalFields/FinalFields.java 2118 java/beans/XMLDecoder/spec/TestNew.java @@ -3150,6 +3186,7 @@ 1994 java/lang/Integer/Decode.java 1993 java/math/BigInteger/ProbablePrime.java 1992 java/lang/Class/getField/ArrayLength.java +1992 java/nio/file/Files/SetLastModifiedTime.java 1988 java/util/Calendar/Bug6645263.java 1987 java/io/Serializable/NPEProvoker/NPEProvoker.java 1985 java/util/EnumSet/BogusEnumSet.java @@ -3317,6 +3354,7 @@ 1755 java/nio/file/Path/PathOps.java 1754 java/net/URLEncoder/Decoder.java 1751 java/net/URLClassLoader/AddURLTest.java +1745 java/nio/file/Files/walkFileTree/FindTest.java 1744 java/lang/management/RuntimeMXBean/InputArgument.java 1735 java/util/Properties/PropertiesSerialization.java 1726 java/io/charStreams/UTF16.java @@ -3327,7 +3365,9 @@ 1709 java/net/ipv6tests/ScopeTests.java 1705 java/net/URL/OpenConnection.java 1704 java/beans/VetoableChangeSupport/Test4994635.java +1704 java/nio/file/Files/probeContentType/ParallelProbes.java 1702 java/util/Collections/MinMax.java +1702 java/lang/RuntimeTests/exec/Duped.java 1701 java/lang/RuntimePermission/ExitVM.java 1699 java/lang/annotation/EqualityTest.java 1692 java/net/URLClassLoader/B6827999.java @@ -3380,6 +3420,7 @@ 1564 java/lang/ThreadLocal/InitialValue.java 1562 java/net/URLEncoder/URLEncodeDecode.java 1553 java/io/Reader/NullLock.java +1551 java/lang/Thread/virtual/stress/SleepALot.java 1547 java/lang/invoke/DropArgumentsTest.java 1542 java/net/URLDecoder/URLDecoderArgs.java 1540 java/lang/ThreadLocal/ThreadLocalSupplierTest.java @@ -3468,7 +3509,9 @@ 1070 java/nio/file/Files/FileAttributes.java 1038 java/nio/channels/FileChannel/LoopingTruncate.java 1036 java/net/URL/Constructor.java +1021 java/nio/file/Files/TemporaryFiles.java 1019 java/nio/channels/DatagramChannel/StressNativeSignal.java +1015 java/nio/file/DirectoryStream/SecureDS.java 1011 java/nio/channels/SocketChannel/CloseAfterConnect.java 1009 java/nio/Buffer/StringCharBufferSliceTest.java 986 java/nio/channels/FileChannel/ReadFull.java @@ -3486,7 +3529,10 @@ 856 java/lang/invoke/7087570/Test7087570.java 845 java/nio/channels/SocketChannel/Close.java 829 java/nio/channels/Channels/ReadOffset.java +812 java/lang/Thread/virtual/ParkWithFixedThreadPool.java +785 java/io/FileOutputStream/AtomicAppend.java 763 java/nio/channels/ServerSocketChannel/NonBlockingAccept.java +758 java/nio/file/attribute/PosixFileAttributeView/Basic.java 755 java/nio/Buffer/CharAt.java 679 java/util/concurrent/forkjoin/ThrowingRunnable.java 668 java/net/URL/LocaleDependentURLTest.java diff --git a/test/nio.test.ts b/test/nio.test.ts index d41e866..8fcf51e 100644 --- a/test/nio.test.ts +++ b/test/nio.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, truncateSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, truncateSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { MemoryBackend, VFS } from "../src/fs.ts"; @@ -125,6 +125,68 @@ test("a large file on a read-only host mount uses a handle", async () => { } }); +test("an open host file keeps its inode when its path is deleted and recreated", async () => { + const { openFile, fileOf, closeFd, removePath } = await import("../src/natives/io.ts"); + const dir = mkdtempSync(join(tmpdir(), "java-web-pin-")); + const fs = new VFS(); + fs.mount("/", new NodeBackend(dir, true)); + const t = { vm: { fs, opts: {}, state: new Map(), exited: new Promise(() => {}) } } as unknown as JThread; + try { + writeFileSync(join(dir, "file"), "old"); + const fd = openFile(t, "/file", true, false, false); + const f = fileOf(t, fd); + let replacement = -1; + try { + assert.ok(f.contents.pin); + const original = fs.stat("/file")!.ino; + assert.equal(removePath(t, "/file"), true); + replacement = openFile(t, "/file", true, false, false); + assert.notEqual(fs.stat("/file")!.ino, original); + assert.equal(f.ino, original); + } finally { + if (replacement >= 0) closeFd(t, replacement); + closeFd(t, fd); + } + assert.equal(f.contents.pin, undefined); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("opening a host file for writing does not create a missing one", () => { + const dir = mkdtempSync(join(tmpdir(), "java-web-nocreate-")); + try { + assert.throws(() => new NodeBackend(dir, true).openFile("/missing", true, false, false), { code: "ENOENT" }); + assert.equal(existsSync(join(dir, "missing")), false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("a path that reaches a different inode does not share the open file's contents", async () => { + const { openFile, fileOf, closeFd } = await import("../src/natives/io.ts"); + const dir = mkdtempSync(join(tmpdir(), "java-web-share-")); + const fs = new VFS(); + fs.mount("/", new NodeBackend(dir, true)); + const t = { vm: { fs, opts: {}, state: new Map(), exited: new Promise(() => {}) } } as unknown as JThread; + try { + writeFileSync(join(dir, "target"), "old"); + symlinkSync("target", join(dir, "link")); + const first = openFile(t, "/link", false, false, false); + renameSync(join(dir, "target"), join(dir, "moved")); + writeFileSync(join(dir, "target"), "new"); + const second = openFile(t, "/link", false, false, false); + const bytes = new Uint8Array(3); + assert.equal(fileOf(t, second).read(bytes, 0, 3), 3); + assert.equal(new TextDecoder().decode(bytes), "new"); + assert.equal(readFileSync(join(dir, "moved"), "utf8"), "old"); + closeFd(t, second); + closeFd(t, first); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("map0 of a region that does not fit throws OutOfMemoryError for FileChannelImpl to wrap", () => { const backend = memory(); const t = thread(backend);