diff --git a/AGENTS.md b/AGENTS.md index 9dfbba2..bbbe5ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,8 @@ Node, which is how the tests run. `src/net-node.ts` host sockets, DNS and interfaces for Node - `src/node.ts` Node entry, over `build/jdk` or, built as `dist/node.js`, the packaged image (`src/image.ts`); `scripts/build-jdk.sh` builds `build/jdk` +- `src/bootpath.ts` class files on the `-Xbootclasspath/a` and `--patch-module` paths; + `src/posixtz.ts` POSIX `TZ` rules such as `MEZ-1MESZ,M3.5.0,M10.5.0/3` - `src/http-proxy.ts` the browser's virtual HTTP proxy (`fetch`); `scripts/tls/` the `javaweb.tls` pass-through TLS provider it relies on for `https:` diff --git a/docs/node.md b/docs/node.md index 182b075..677c9ad 100644 --- a/docs/node.md +++ b/docs/node.md @@ -18,6 +18,7 @@ npx @project516/java-web [options] -jar app.jar [args] | `-ea`, `-da`, `-esa` and the rest | assertions, as with `java` | | `--enable-preview` | preview features | | `--add-exports`, `--add-opens`, `--add-reads`, `--add-modules`, `--module-path`, `-p` | module options, as with `java` | +| `-m`, `--module module[/class]` | run a main class from a module on the module path | | `--locale-data` | locales other than English (see [support.md](support.md#locales)) | | `-Xmx` | the maximum heap reported by `Runtime.maxMemory()` (at most 1g, rounded up to 2 MB steps, below 2m an error). Under Node, java-web checks measured heap use periodically during allocation and throws `OutOfMemoryError: Java heap space` when live use stays over the cap after collection. Without an exposed garbage collector, it checks after estimated allocations exceed the cap. Pages set the cap with `maxHeap` ([browser.md](browser.md)). | | `--max-rss MB` | exit when resident memory passes this (default 2048) | @@ -26,6 +27,9 @@ npx @project516/java-web [options] -jar app.jar [args] Other `-X` and HotSpot-only options are ignored. +`kill -QUIT` on a running command prints every thread's stack to stderr, as +HotSpot does, and the program goes on. `JW_DUMP_STACKS=10` does it every 10 seconds. + ## From JS ```js @@ -43,7 +47,7 @@ const code = await runNode({ The package ships TypeScript declarations (`dist/node.d.ts`). `runNode` resolves to the exit code. Its options: -- `classpath`, `mainClass` or `jar`, `args`, `props` (system properties) +- `classpath`, `mainClass`, `mainModule` (`module/class`, with `jdk.module.path` in `props`) or `jar`, `args`, `props` (system properties) - `stdout`, `stderr` (byte callbacks; default the process's), `input` (fixed stdin bytes) - `env` (what `System.getenv()` returns; default the host's environment) - `natives`: Java `native` methods in JS, as in the browser ([browser.md](browser.md)) diff --git a/docs/support.md b/docs/support.md index 72f5a62..815d467 100644 --- a/docs/support.md +++ b/docs/support.md @@ -128,3 +128,8 @@ Locales other than English need `cheerpjInit({ localeData: true })`, or - IPv4 only. Not supported: IPv6, asynchronous channels (`AsynchronousSocketChannel`), SCTP, ICMP (`isReachable` is true for the host's own addresses only) and inherited channels. +- `jdk.net` is in the image. Its socket options (TCP keep-alive time, interval + and probes, quick ack, don't fragment) keep their values on the socket and do + not reach a host socket. Peer credentials are this process's user and group + when the peer is in the same VM, and -1 for both when it is a host socket. TCP urgent data (`sendUrgentData`, + `MSG_OOB`) is not supported: Node cannot send or read it. diff --git a/scripts/build-jdk.sh b/scripts/build-jdk.sh index 0ca65a9..9eda057 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.management,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.net,jdk.nio.mapmode,jdk.internal.md,javaweb.tls} # Builds from one pinned JDK release so every machine makes the same image. # Renovate bumps the release. @@ -125,6 +125,20 @@ mkdir -p "$out/lib/fonts" cp "$font_home/ttf/DejaVuSans.ttf" "$out/lib/fonts/" cp "$font_home/LICENSE" "$out/legal/java.desktop/dejavu-fonts.txt" +# Programs look for bin/java under java.home. ProcessBuilder starts the VM's +# own java for this path; run from a host shell, it starts the java-web command, +# or the checkout the image was built in. +mkdir -p "$out/bin" +cat > "$out/bin/java" <<'SH' +#!/bin/sh +command -v java-web >/dev/null 2>&1 && exec java-web "$@" +root=$(cd "$(dirname "$(readlink -f "$0")")/../../.." && pwd) +[ -f "$root/src/node.ts" ] && exec node "$root/src/node.ts" "$@" +echo "java: the java-web command is not on PATH" >&2 +exit 127 +SH +chmod 755 "$out/bin/java" + # a digest of the whole image, which boot snapshots are made for (cd "$out" && node -e ' const { createHash } = require("node:crypto"); diff --git a/src/bootpath.ts b/src/bootpath.ts new file mode 100644 index 0000000..6726d91 --- /dev/null +++ b/src/bootpath.ts @@ -0,0 +1,70 @@ +// Class files on the -Xbootclasspath/a path: directories and jars on the host. + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { inflateRawSync } from "node:zlib"; + +type Jar = { buf: Buffer; entries: Map }; + +function readJar(file: string): Jar | null { + let buf: Buffer; + try { buf = fs.readFileSync(file); } catch { return null; } + // a jar that cannot be read has no classes, as a malformed one on HotSpot's path + try { + let end = buf.length - 22; + while (end >= 0 && buf.readUInt32LE(end) !== 0x06054b50) end--; + if (end < 0) return null; + const entries = new Map(); + let p = buf.readUInt32LE(end + 16); + for (let n = buf.readUInt16LE(end + 10); n > 0 && buf.readUInt32LE(p) === 0x02014b50; n--) { + const nameLen = buf.readUInt16LE(p + 28); + entries.set(buf.toString("utf8", p + 46, p + 46 + nameLen), { method: buf.readUInt16LE(p + 10), size: buf.readUInt32LE(p + 20), at: buf.readUInt32LE(p + 42) }); + p += 46 + nameLen + buf.readUInt16LE(p + 30) + buf.readUInt16LE(p + 32); + } + return { buf, entries }; + } catch { + return null; + } +} + +function fromJar(jar: Jar, name: string): Uint8Array | null { + const e = jar.entries.get(name); + if (!e) return null; + try { + const start = e.at + 30 + jar.buf.readUInt16LE(e.at + 26) + jar.buf.readUInt16LE(e.at + 28); + const data = jar.buf.subarray(start, start + e.size); + return e.method === 0 ? data : e.method === 8 ? inflateRawSync(data) : null; + } catch { + return null; + } +} + +// entries is the colon-separated -Xbootclasspath/a value +export function bootClassPath(entries: string | undefined): ((name: string) => { bytes: Uint8Array; location: string } | null) | undefined { + if (!entries) return undefined; + const list = entries.split(":").filter(Boolean).map((e) => ({ entry: path.resolve(e), jar: undefined as Jar | null | undefined })); + return (name) => { + for (const e of list) { + if (e.jar === undefined) e.jar = fs.statSync(e.entry, { throwIfNoEntry: false })?.isDirectory() ? null : readJar(e.entry); + if (e.jar) { + const bytes = fromJar(e.jar, `${name}.class`); + if (bytes) return { bytes, location: e.entry }; + } else if (e.jar === null) { + try { return { bytes: fs.readFileSync(path.join(e.entry, `${name}.class`)), location: e.entry }; } catch { /* not here */ } + } + } + return null; + }; +} + +// --patch-module module=path:path, as the jdk.module.patch.N properties hold it +export function bootPatches(props: Record | undefined): ((module: string, name: string) => Uint8Array | null) | undefined { + const paths = new Map(); + for (const [k, v] of Object.entries(props ?? {})) { + const eq = v.indexOf("="); + if (/^jdk\.module\.patch\.\d+$/.test(k) && eq > 0) paths.set(v.slice(0, eq), [...(paths.get(v.slice(0, eq)) ?? []), v.slice(eq + 1)]); + } + if (paths.size === 0) return undefined; + const finders = new Map([...paths].map(([module, list]) => [module, bootClassPath(list.join(":"))!])); + return (module, name) => finders.get(module)?.(name)?.bytes ?? null; +} diff --git a/src/indy.ts b/src/indy.ts index f1666fe..1afbc6f 100644 --- a/src/indy.ts +++ b/src/indy.ts @@ -140,6 +140,16 @@ function* defineHidden(vm: JVM, t: JThread, caller: JClass, w: ClassWriter): Nat // ---- LambdaMetafactory ----------------------------------------------------- +// -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles: the JDK's own dumper writes the class +function* dumpLambda(vm: JVM, t: JThread, c: JClass, bytes: Uint8Array): NativeGen { + const factory = vm.loadBootOrFail("java/lang/invoke/InnerClassLambdaMetafactory"); + if (factory.state !== INITIALIZED) yield* vm.initGen(t, factory); + const array = vm.newArray(vm.primClass("B"), bytes.length); + (array.a as Int8Array).set(new Int8Array(bytes.buffer, bytes.byteOffset, bytes.length)); + yield t.callVirtual(vm.getStatic(factory, "lambdaProxyClassFileDumper"), "dumpClass", "(Ljava/lang/String;Ljava/lang/Class;[B)V", + vm.str(c.cf!.name), vm.mirror(c), array); +} + function* spinLambda(vm: JVM, t: JThread, caller: JClass, name: string, factoryDesc: string, args: BsmArg[], alt: boolean): NativeGen { const sam = args[0], impl = args[1], inst = args[2]; if (sam.kind !== "mt" || impl.kind !== "mh" || inst.kind !== "mt") return null; @@ -223,6 +233,7 @@ function* spinLambda(vm: JVM, t: JThread, caller: JClass, name: string, factoryD m.ret(samRet); } const c: JClass = yield* defineHidden(vm, t, caller, w); + if (vm.props.has("jdk.invoke.LambdaMetafactory.dumpProxyClassFiles")) yield* dumpLambda(vm, t, c, w.toBytes()); // HotSpot's lambda classes have neither; its call sites build the instance const factoryMethod = c.methodMap.get("get$Lambda" + factoryDesc)!; c.vmMembers = new Set([factoryMethod, ...c.fields.filter((f) => f.name === "INSTANCE")]); diff --git a/src/interp.ts b/src/interp.ts index 13c5f43..5e9012c 100644 --- a/src/interp.ts +++ b/src/interp.ts @@ -17,7 +17,9 @@ const MAX_FRAMES = 6000; // be constructed and handled; the limit drops back once the stack unwinds. function checkStack(t: JThread) { if (t.frames.length < t.stackLimit) return; - if (t.stackLimit >= 2 * MAX_FRAMES) throw new Error("stack overflow while handling a stack overflow"); + // Overflows inside handlers of overflows have no headroom left to build an + // error with its constructor, so a bare one is thrown. + if (t.stackLimit >= 2 * MAX_FRAMES) throw new JavaThrow(t.vm.newObject(t.vm.loadBootOrFail("java/lang/StackOverflowError"))); t.stackLimit = t.frames.length + 1500; throw new PendingThrow("java/lang/StackOverflowError"); } diff --git a/src/jvm.ts b/src/jvm.ts index 8b99130..b24aa26 100644 --- a/src/jvm.ts +++ b/src/jvm.ts @@ -65,6 +65,12 @@ export interface JvmOptions { assertions?: Assertions; // run class files that use this release's preview features enablePreview?: boolean; + // class bytes from --patch-module for a boot loader module, which win over the image's + bootPatch?: (module: string, name: string) => Uint8Array | null; + // class bytes on the -Xbootclasspath/a path, and the path entry that holds them + bootClassPath?: (name: string) => { bytes: Uint8Array; location: string } | null; + // os.version; the kernel release where there is one + osVersion?: string; // sockets and name lookup; defaults to loopback sockets inside the VM net?: NetBackend; // what InetAddress.getLocalHost() reports @@ -97,7 +103,7 @@ export interface JvmOptions { const RESUMED = Object.freeze({ kind: "resume" }) as unknown as Request; // LauncherHelper launch modes -export const LM_CLASS = 1, LM_JAR = 2; +export const LM_CLASS = 1, LM_JAR = 2, LM_MODULE = 3; const intrinsicClasses = new Set(Object.keys(intrinsics).map((k) => k.slice(0, k.indexOf(".")))); @@ -105,8 +111,8 @@ const intrinsicClasses = new Set(Object.keys(intrinsics).map((k) => k.slice(0, k // snapshot is dropped, so a new VM with the same options boots. export class SnapshotError extends Error {} -// properties under which boot reads application files, which a key does not cover -const BOOT_READS_APP = /^(java\.system\.class\.loader|jdk\.module\.(path|upgrade\.path|patch\.\d+))$/; +// properties under which the VM reads application files that a snapshot key does not cover +const BOOT_READS_APP = /^(java\.system\.class\.loader|jdk\.boot\.class\.path\.append|jdk\.module\.(path|upgrade\.path|patch\.\d+))$/; const YIELD = (() => { if (typeof setImmediate === "function") return (fn: () => void) => setImmediate(fn); @@ -131,6 +137,10 @@ export class JVM { private readonly image: JImage | null; // package -> module, for boot classes readonly bootPackages = new Map(); + // packages of modules defined at run time for the boot loader, such as a Proxy's dynamic module + readonly bootModulePackages = new Map(); + // packages of classes loaded from -Xbootclasspath/a, with the path entry they came from + readonly bootAppendPackages = new Map(); // Module objects defined to the boot loader, by name readonly bootModules = new Map(); bootUnnamedModule: JObject | null = null; @@ -197,10 +207,12 @@ export class JVM { // Locale data is left out as --limit-modules would leave it, unless asked // for, or named by --add-modules (by name or as ALL-SYSTEM). A limit given // by the caller keeps it only if it names it, since no module requires it. + // With a module path it stays in: a limit would hide the modules the main + // module requires. const addmods = [...this.props].filter(([k]) => k.startsWith("jdk.module.addmods.")).flatMap(([, v]) => v.split(",")); const limit = this.props.get("jdk.module.limitmods"); const noLocales = this.image !== null && (limit !== undefined ? !limit.split(",").includes("jdk.localedata") - : !opts.localeData && !addmods.some((m) => m === "ALL-SYSTEM" || m === "jdk.localedata")); + : !opts.localeData && !this.props.has("jdk.module.path") && !addmods.some((m) => m === "ALL-SYSTEM" || m === "jdk.localedata")); const present = new Map(); for (const line of new TextDecoder().decode(index).split("\n")) { const sp = line.indexOf(" "); @@ -250,8 +262,19 @@ export class JVM { } const i = name.lastIndexOf("/"); const module = this.bootPackages.get(i < 0 ? "" : name.slice(0, i)); - if (!module) return null; - const bytes = this.image ? this.image.read(module, `${name}.class`) : this.fs.read(`${this.opts.javaHome}/modules/${module}/${name}.class`); + // once the boot layer is defined, a module left out of it (--limit-modules) has no classes + if (module && this.bootModules.has("java.base") && !this.bootModules.has(module)) return null; + let bytes: Uint8Array | null | undefined; + if (module) { + bytes = this.opts.bootPatch?.(module, name) ?? (this.image ? this.image.read(module, `${name}.class`) : this.fs.read(`${this.opts.javaHome}/modules/${module}/${name}.class`)); + } + else { + const found = this.opts.bootClassPath?.(name); + if (found) { + bytes = found.bytes; + this.bootAppendPackages.set(i < 0 ? "" : name.slice(0, i), found.location); + } + } if (!bytes) return null; const cf = parseClass(bytes); const superclass = cf.superName ? this.loadBootOrFail(cf.superName) : null; @@ -715,7 +738,7 @@ export class JVM { const mod = this.bootPackages.get(c.packageName); // a module not defined yet: defineModule0 sets it later, as HotSpot does if (mod) return this.bootModules.get(mod) ?? null; - return this.bootUnnamedModule; + return this.bootModulePackages.get(c.packageName) ?? this.bootUnnamedModule; } const named = this.loaderData(c.loader).modules.get(c.packageName); if (named) return named; @@ -880,6 +903,16 @@ export class JVM { this.throwNew(t, "java/lang/InternalError", e instanceof Error ? e.message : String(e)); } + // Every live thread's Java stack, for kill -QUIT on a stuck program + dumpAllStacks() { + console.error(`Full thread dump ${this.props.get("java.vm.name") ?? "java-web"}:`); + for (const t of this.threads) { + if (t.state === TERMINATED) continue; + console.error(`\n"${t.name}" ${t.state === RUNNABLE ? "runnable" : "waiting"}`); + this.dumpStack(t); + } + } + dumpStack(t: JThread) { for (let i = t.frames.length - 1; i >= 0; i--) { const f = t.frames[i]; @@ -1167,6 +1200,10 @@ export class JVM { failed(e instanceof Error ? e : new Error(String(e))); return; } + if (this.exitCode !== null) { + failed(new Error(`VM exited during startup with ${this.exitCode}`)); + return; + } if (this.opts.warmSnapshot && this.libraryStarting && this.snapshots) { this.warmTraceActive = this.fs.beginWarmTrace(); this.warmTraceClasses = this.classCount; @@ -1260,8 +1297,12 @@ export class JVM { "java/lang/invoke/MemberName", "java/lang/invoke/MethodHandleNatives", ]) yield* init(n); this.debug("initPhase2"); - const rc = yield t.call(boot("java/lang/System").methodMap.get("initPhase2(ZZ)I")!, 1, 1); - if (rc !== 0) throw new Error(`initPhase2 failed: ${rc}`); + // HotSpot prints a failed boot layer to stdout, without a stack trace, and exits + const rc = yield t.call(boot("java/lang/System").methodMap.get("initPhase2(ZZ)I")!, 0, 0); + if (rc !== 0) { + this.halt(1); + return; + } this.debug("initPhase3"); yield t.call(boot("java/lang/System").methodMap.get("initPhase3()V")!); this.debug("vm started in", Date.now() - this.startTime, "ms"); @@ -1355,6 +1396,7 @@ export class JVM { if (!restored) { if (cmd !== undefined) this.props.set("sun.java.command", ""); yield* this.bootGen(t); + if (this.exitCode !== null) return; // a snapshot is only a cache: failing to make or keep one never fails the run if (snaps) { try { @@ -1382,6 +1424,7 @@ export class JVM { private *mainGen(t: JThread, mode: number, what: string, args: string[], restored: boolean): NativeGen { const boot = (n: string) => this.loadBootOrFail(n); yield* this.startGen(t, restored); + if (this.exitCode !== null) return; let code = 0; try { const helper = boot("sun/launcher/LauncherHelper"); diff --git a/src/natives/loader.ts b/src/natives/loader.ts index 4dfab8b..8bfc3a2 100644 --- a/src/natives/loader.ts +++ b/src/natives/loader.ts @@ -25,7 +25,10 @@ function defineModule(t: JThread, m: JObject, version: JObject | null, location: const loader = vm.getField(m, "java/lang/Module", "loader") as JObject | null; const pkgs = (pns.a as JObject[]).map((p) => vm.jsString(p).replace(/\./g, "/")); if (loader === null) { - if (name) vm.bootModules.set(name, m); + if (name) { + vm.bootModules.set(name, m); + for (const p of pkgs) if (!vm.bootPackages.has(p)) vm.bootModulePackages.set(p, m); + } const set = new Set(pkgs); for (const c of [...vm.boot.values(), ...vm.prims.values()]) { let e = c; diff --git a/src/natives/management.ts b/src/natives/management.ts index e62960d..50ec8f5 100644 --- a/src/natives/management.ts +++ b/src/natives/management.ts @@ -1,9 +1,8 @@ // java.management natives: what HotSpot's JMM interface (services/management.cpp) // reports, for a VM with green threads on the JS engine's collector. Memory -// managers and pools have the names HotSpot gives them with the Serial -// collector, which it picks for a heap of java-web's size. Heap usage is the JS -// heap's; collections are the explicit System.gc() calls, which HotSpot counts -// as full collections. +// managers and pools have the names HotSpot gives them with G1, its default +// collector. Heap usage is the JS heap's; collections are the explicit +// System.gc() calls, which HotSpot counts as full collections. import { INITIALIZED, JArray, JClass, JObject, MAX_HEAP } from "../runtime.ts"; import type { JMethod } from "../runtime.ts"; @@ -19,25 +18,27 @@ import { continuationEntry } from "./continuation.ts"; import { fillElement } from "./lang.ts"; import type { Natives } from "./util.ts"; +// the memory managers and pools of HotSpot's default collector, G1 const MANAGERS = [ { name: "CodeCacheManager", gc: false, pools: ["CodeHeap 'profiled nmethods'", "CodeHeap 'non-nmethods'", "CodeHeap 'non-profiled nmethods'"] }, { name: "Metaspace Manager", gc: false, pools: ["Metaspace", "Compressed Class Space"] }, - { name: "Copy", gc: true, pools: ["Eden Space", "Survivor Space"] }, - { name: "MarkSweepCompact", gc: true, pools: ["Eden Space", "Survivor Space", "Tenured Gen"] }, + { name: "G1 Young Generation", gc: true, pools: ["G1 Eden Space", "G1 Survivor Space", "G1 Old Gen"] }, + { name: "G1 Concurrent GC", gc: true, pools: ["G1 Old Gen"] }, + { name: "G1 Old Generation", gc: true, pools: ["G1 Eden Space", "G1 Survivor Space", "G1 Old Gen"] }, ]; // in HotSpot's order, with whether each supports usage and collection thresholds const POOLS = [ { name: "CodeHeap 'non-nmethods'", heap: false, usage: true, collection: false }, { name: "Metaspace", heap: false, usage: true, collection: false }, - { name: "Tenured Gen", heap: true, usage: true, collection: true }, { name: "CodeHeap 'profiled nmethods'", heap: false, usage: true, collection: false }, - { name: "Eden Space", heap: true, usage: false, collection: true }, - { name: "Survivor Space", heap: true, usage: false, collection: true }, { name: "Compressed Class Space", heap: false, usage: true, collection: false }, + { name: "G1 Eden Space", heap: true, usage: false, collection: true }, + { name: "G1 Old Gen", heap: true, usage: true, collection: true }, + { name: "G1 Survivor Space", heap: true, usage: false, collection: true }, { name: "CodeHeap 'non-profiled nmethods'", heap: false, usage: true, collection: false }, ]; -const FULL_GC = "MarkSweepCompact"; -const OLD = "Tenured Gen"; +const FULL_GC = "G1 Old Generation"; +const OLD = "G1 Old Gen"; interface Beans { pools: JObject[]; @@ -137,15 +138,16 @@ function procValue(file: string, label: string, field = 1): number { 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 +// The VM options java-web can state, as HotSpot reports them with G1 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: "UseG1GC", value: true, writeable: false }, { name: "UseParallelGC", value: false, writeable: false }, - { name: "UseSerialGC", value: true, writeable: false }, + { name: "UseSerialGC", value: false, writeable: false }, ]; const TH = "java/lang/Thread"; @@ -551,7 +553,8 @@ export const natives: Natives = { [`${P}getMemoryManagers0()[Ljava/lang/management/MemoryManagerMXBean;`]: function* (t, [self]): NativeGen { const b: Beans = yield* beans(t); const name = POOLS[b.pools.indexOf(self)].name; - return t.vm.newObjectArray("java/lang/management/MemoryManagerMXBean", b.managers.filter((_, i) => MANAGERS[i].pools.includes(name))); + // HotSpot lists a pool's managers last added first + return t.vm.newObjectArray("java/lang/management/MemoryManagerMXBean", b.managers.filter((_, i) => MANAGERS[i].pools.includes(name)).reverse()); }, [`${P}setPoolUsageSensor(Lsun/management/Sensor;)V`]: () => undefined, [`${P}setPoolCollectionSensor(Lsun/management/Sensor;)V`]: () => undefined, diff --git a/src/natives/misc.ts b/src/natives/misc.ts index 19a2c28..e2fe900 100644 --- a/src/natives/misc.ts +++ b/src/natives/misc.ts @@ -239,7 +239,14 @@ Object.assign(unsafe, { [`${U}ensureClassInitialized0(Ljava/lang/Class;)V`]: (t: JThread, [, c]: any[]) => t.vm.initGen(t, t.vm.classOf(c)), [`${U}arrayBaseOffset0(Ljava/lang/Class;)I`]: () => ARRAY_BASE_OFFSET, [`${U}arrayIndexScale0(Ljava/lang/Class;)I`]: (t: JThread, [, c]: any[]) => indexScale(t.vm.classOf(c)), - [`${U}getLoadAverage0([DI)I`]: () => -1, + [`${U}getLoadAverage0([DI)I`]: (_t, [, a, n]) => { + // the host's 1, 5 and 15 minute averages under Node; none in a browser + const load: number[] | undefined = (globalThis as any).process?.getBuiltinModule?.("node:os")?.loadavg(); + if (!load || process.platform === "win32") return -1; + const count = Math.min(n, 3, (a as JArray).a.length); + for (let i = 0; i < count; i++) (a as JArray).a[i] = load[i]; + return count; + }, [`${U}getUncompressedObject(J)Ljava/lang/Object;`]: () => null, [`${U}throwException(Ljava/lang/Throwable;)V`]: (_t: JThread, [, e]: any[]) => { throw new JavaThrow(nonNull(e)); }, [`${U}allocateInstance(Ljava/lang/Class;)Ljava/lang/Object;`]: function* (t: JThread, [, m]: any[]): NativeGen { @@ -276,7 +283,7 @@ export function platformProperties(vm: JVM): (string | null)[] { p.get("java.io.tmpdir") ?? "/tmp", "\n", "UTF-8", // native.encoding - "javascript", "Linux", "6.0", + "javascript", "Linux", vm.opts.osVersion ?? "6.0", ":", null, null, null, // socks "UTF-8", "UTF-8", "UTF-8", // stderr, stdin, stdout encoding @@ -436,10 +443,12 @@ export const natives: Natives = { }, "jdk/internal/loader/BootLoader.getSystemPackageNames()[Ljava/lang/String;": (t) => - t.vm.newObjectArray("java/lang/String", [...t.vm.bootPackages.keys()].map((p) => t.vm.str(p))), + t.vm.newObjectArray("java/lang/String", [...t.vm.bootPackages.keys(), ...t.vm.bootAppendPackages.keys()].map((p) => t.vm.str(p))), "jdk/internal/loader/BootLoader.getSystemPackageLocation(Ljava/lang/String;)Ljava/lang/String;": (t, [n]) => { - const mod = t.vm.bootPackages.get(t.vm.jsString(n)); - return mod ? t.vm.str(`jrt:/${mod}`) : null; + const pkg = t.vm.jsString(n); + const mod = t.vm.bootPackages.get(pkg); + const appended = t.vm.bootAppendPackages.get(pkg); + return mod ? t.vm.str(`jrt:/${mod}`) : appended ? t.vm.str(appended) : null; }, "jdk/internal/loader/BootLoader.setBootLoaderUnnamedModule0(Ljava/lang/Module;)V": (t, [m]) => { t.vm.bootUnnamedModule = m; diff --git a/src/natives/net.ts b/src/natives/net.ts index 74a8f3b..e84d96f 100644 --- a/src/natives/net.ts +++ b/src/natives/net.ts @@ -4,6 +4,7 @@ import { INITIALIZED } from "../runtime.ts"; import type { JArray, JClass, JObject } from "../runtime.ts"; import { PendingThrow } from "../thread.ts"; import type { JThread, NativeGen } from "../thread.ts"; +import { posixOffsetMinutes } from "../posixtz.ts"; import { ipv4Literal, isLoopback, isWildcard, sameAddr } from "../sockets.ts"; import type { Endpoint, NetIf } from "../sockets.ts"; import type { Natives } from "./util.ts"; @@ -145,17 +146,19 @@ export const natives: Natives = { "java/net/Inet6AddressImpl.getHostByAddr([B)Ljava/lang/String;": (t, [, addr]) => hostByAddr(t, addr), "java/net/Inet6AddressImpl.isReachable0([BII[BII)Z": (t, [, addr]) => reachable(t, addr), + // TZ names the zone as in libc; a value that is no zone ID (a POSIX rule such as + // "MEZ-1MESZ,M3.5.0,M10.5.0/3") makes Java ask for the current GMT offset "java/util/TimeZone.getSystemTimeZoneID(Ljava/lang/String;)Ljava/lang/String;": (t) => { - let id = "UTC"; + const tz = t.vm.opts.env?.TZ?.replace(/^:/, ""); try { - id = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + return t.vm.str(Intl.DateTimeFormat(undefined, tz ? { timeZone: tz } : undefined).resolvedOptions().timeZone || "UTC"); } catch { - // no Intl time zone support + return t.vm.str(tz || "UTC"); } - return t.vm.str(id); }, "java/util/TimeZone.getSystemGMTOffsetID()Ljava/lang/String;": (t) => { - const off = -new Date().getTimezoneOffset(); + const tz = t.vm.opts.env?.TZ?.replace(/^:/, ""); + const off = (tz ? posixOffsetMinutes(tz, Date.now()) : null) ?? -new Date().getTimezoneOffset(); const a = Math.abs(off); const pad = (n: number) => String(n).padStart(2, "0"); return t.vm.str(`GMT${off < 0 ? "-" : "+"}${pad(Math.floor(a / 60))}:${pad(a % 60)}`); diff --git a/src/natives/reflect.ts b/src/natives/reflect.ts index c0b1eed..06efc1e 100644 --- a/src/natives/reflect.ts +++ b/src/natives/reflect.ts @@ -482,36 +482,50 @@ for (const [name, d] of Object.entries({ Boolean: "Z", Byte: "B", Char: "C", Sho }; } +// ConstantPool's natives check the index and the entry's kind as HotSpot does +function cpAt(t: JThread, o: JObject, i: number, ...tags: number[]) { + const cp = cpOf(t, o); + if (i < 0 || i >= cp.length) throw new PendingThrow("java/lang/IllegalArgumentException", "Constant pool index out of bounds"); + const e = cpEntry(cp, i); + if (tags.length && (!e || !tags.includes(e.tag))) throw new PendingThrow("java/lang/IllegalArgumentException", "Wrong type at constant pool index"); + return { cp, e: e! }; +} +const FIELD_OR_METHOD = [Tag.Fieldref, Tag.Methodref, Tag.InterfaceMethodref]; + Object.assign(natives, { [`${CP}getSize0(Ljava/lang/Object;)I`]: (t: JThread, [, o]: any[]) => cpOf(t, o).length, - [`${CP}getIntAt0(Ljava/lang/Object;I)I`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)!.value, - [`${CP}getLongAt0(Ljava/lang/Object;I)J`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)!.value, - [`${CP}getFloatAt0(Ljava/lang/Object;I)F`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)!.value, - [`${CP}getDoubleAt0(Ljava/lang/Object;I)D`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)!.value, - [`${CP}getUTF8At0(Ljava/lang/Object;I)Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => t.vm.intern(utf8(cpOf(t, o), i)), - [`${CP}getStringAt0(Ljava/lang/Object;I)Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => t.vm.intern(utf8(cpOf(t, o), cpEntry(cpOf(t, o), i)!.a)), - [`${CP}getTagAt0(Ljava/lang/Object;I)B`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)?.tag ?? 0, + [`${CP}getIntAt0(Ljava/lang/Object;I)I`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i, Tag.Integer).e.value, + [`${CP}getLongAt0(Ljava/lang/Object;I)J`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i, Tag.Long).e.value, + [`${CP}getFloatAt0(Ljava/lang/Object;I)F`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i, Tag.Float).e.value, + [`${CP}getDoubleAt0(Ljava/lang/Object;I)D`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i, Tag.Double).e.value, + [`${CP}getUTF8At0(Ljava/lang/Object;I)Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => t.vm.intern(utf8(cpAt(t, o, i, Tag.Utf8).cp, i)), + [`${CP}getStringAt0(Ljava/lang/Object;I)Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => { + const { cp, e } = cpAt(t, o, i, Tag.String); + return t.vm.intern(utf8(cp, e.a)); + }, + [`${CP}getTagAt0(Ljava/lang/Object;I)B`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i).e?.tag ?? 0, [`${CP}getClassAt0(Ljava/lang/Object;I)Ljava/lang/Class;`]: function* (t: JThread, [, o, i]: any[]): NativeGen { const c = t.vm.classOf(o); + cpAt(t, o, i, Tag.Class); return t.vm.mirror(yield* t.vm.loadClassGen(t, c.loader, className(c.cf!.cp, i))); }, [`${CP}getClassAtIfLoaded0(Ljava/lang/Object;I)Ljava/lang/Class;`]: (t: JThread, [, o, i]: any[]) => { const c = t.vm.classOf(o); + cpAt(t, o, i, Tag.Class); const found = t.vm.findLoaded(c.loader, className(c.cf!.cp, i)); return found ? t.vm.mirror(found) : null; }, [`${CP}getMemberRefInfoAt0(Ljava/lang/Object;I)[Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => { - const cp = cpOf(t, o); - const e = cpEntry(cp, i)!, nat = cpEntry(cp, e.b)!; + const { cp, e } = cpAt(t, o, i, ...FIELD_OR_METHOD); + const nat = cpEntry(cp, e.b)!; return t.vm.newObjectArray("java/lang/String", [className(cp, e.a), utf8(cp, nat.a), utf8(cp, nat.b)].map((s) => t.vm.intern(s))); }, [`${CP}getNameAndTypeRefInfoAt0(Ljava/lang/Object;I)[Ljava/lang/String;`]: (t: JThread, [, o, i]: any[]) => { - const cp = cpOf(t, o); - const e = cpEntry(cp, i)!; + const { cp, e } = cpAt(t, o, i, Tag.NameAndType); return t.vm.newObjectArray("java/lang/String", [utf8(cp, e.a), utf8(cp, e.b)].map((s) => t.vm.intern(s))); }, - [`${CP}getClassRefIndexAt0(Ljava/lang/Object;I)I`]: (t: JThread, [, o, i]: any[]) => cpEntry(cpOf(t, o), i)!.a, + [`${CP}getClassRefIndexAt0(Ljava/lang/Object;I)I`]: (t: JThread, [, o, i]: any[]) => cpAt(t, o, i, ...FIELD_OR_METHOD).e.a, [`${CP}getNameAndTypeRefIndexAt0(Ljava/lang/Object;I)I`]: (t: JThread, [, o, i]: any[]) => { - return cpEntry(cpOf(t, o), i)!.b; + return cpAt(t, o, i, ...FIELD_OR_METHOD, Tag.InvokeDynamic).e.b; }, }); diff --git a/src/natives/sockets.ts b/src/natives/sockets.ts index 193d507..f493d52 100644 --- a/src/natives/sockets.ts +++ b/src/natives/sockets.ts @@ -171,6 +171,41 @@ export function socketError(errno: number, msg?: string): PendingThrow { } const ioError = (errno: number) => new PendingThrow("java/io/IOException", strerror(errno)); +// jdk.net's ExtendedSocketOptions on Linux, kept with the socket's options; the host sockets +// do not take the values. Linux's defaults are 7200 s, 75 s and 9 probes. +function extendedOptions(): Natives { + const L = "jdk/net/LinuxSocketOptions."; + const TCP = 6, IP = 0, IPV6 = 41; + const bound = (t: JThread, fd: number) => { + const s = socksOf(t.vm.state).get(fd); + if (!s) throw new PendingThrow("java/net/SocketException", strerror(EBADF)); + return s; + }; + const out: Natives = {}; + for (const [name, opt, dflt] of [["TcpKeepAliveProbes", 6, 9], ["TcpKeepAliveTime", 4, 7200], ["TcpKeepAliveIntvl", 5, 75]] as const) { + out[`${L}set${name}0(II)V`] = (t, [fd, v]) => { bound(t, fd).setOption(TCP, opt, v); }; + out[`${L}get${name}0(I)I`] = (t, [fd]) => bound(t, fd).option(TCP, opt, dflt); + } + out[`${L}setQuickAck0(IZ)V`] = (t, [fd, v]) => { bound(t, fd).setOption(TCP, 12, v); }; + out[`${L}getQuickAck0(I)Z`] = (t, [fd]) => bound(t, fd).option(TCP, 12, 1); + // IP_MTU_DISCOVER: 2 is "do not fragment" + out[`${L}setIpDontFragment0(IZZ)V`] = (t, [fd, v, v6]) => { bound(t, fd).setOption(v6 ? IPV6 : IP, v6 ? 23 : 10, v ? 2 : 0); }; + out[`${L}getIpDontFragment0(IZ)Z`] = (t, [fd, v6]) => (bound(t, fd).option(v6 ? IPV6 : IP, v6 ? 23 : 10, 0) === 2 ? 1 : 0); + out[`${L}getSoPeerCred0(I)J`] = (t, [fd]) => { + const s = bound(t, fd); + if (!s.connected) throw new PendingThrow("java/net/SocketException", strerror(ENOTCONN)); + // a peer in this VM is this process; a host peer's ids are unknown, -1 as Linux gives without credentials + if (!s.peer && !s.peerInVm) return -1n; + const p = (globalThis as any).process; + return (BigInt(p?.getuid?.() ?? 0) << 32n) | BigInt(p?.getgid?.() ?? 0); + }; + out[`${L}keepAliveOptionsSupported0()Z`] = () => 1; + out[`${L}quickAckSupported0()Z`] = () => 1; + out[`${L}incomingNapiIdSupported0()Z`] = () => 1; + out[`${L}getIncomingNapiId0(I)I`] = (t, [fd]) => { bound(t, fd); return 0; }; + return out; +} + // 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"); @@ -647,7 +682,8 @@ export const natives: Natives = { }, [`${N}connect0(ZLjava/io/FileDescriptor;Ljava/net/InetAddress;I)I`]: function* (t, [, fd, ia, port]): NativeGen { const s = sockOf(t, fd); - if (s.connected || s.connecting) throw socketError(106); + // a datagram socket may connect again + if (s.stream && (s.connected || s.connecting)) throw socketError(106); const e = yield* done(t.vm.net.connect(s, { addr: inetBytes(t, ia), port }), t); if (e && e !== 115) throw socketError(e); if (s.connecting) { @@ -798,6 +834,8 @@ export const natives: Natives = { [`${NSA}offsetSin6ScopeId()I`]: () => 24, [`${NSA}offsetSin6FlowInfo()I`]: () => 4, + ...extendedOptions(), + "sun/nio/ch/UnixDomainSockets.init()Z": () => 1, "sun/nio/ch/UnixDomainSockets.socket0()I": (t) => { const s = new Sock("stream"); @@ -806,8 +844,10 @@ export const natives: Natives = { }, "sun/nio/ch/UnixDomainSockets.bind0(Ljava/io/FileDescriptor;[B)V": function* (t, [fd, path]): NativeGen { const s = sockOf(t, fd); - const p = unixFile(t, path); if (s.localPath) throw socketError(EINVAL); + // an empty path is the unnamed address; Linux binds it to an abstract name Java reports as unnamed + if (path.a.length === 0) return; + const p = unixFile(t, path); if (!t.vm.net.hostUnix) { if (t.vm.fs.stat(p)) throw socketError(EADDRINUSE); if (!t.vm.fs.write(p, new Uint8Array(0))) throw socketError(EACCES); diff --git a/src/net-node.ts b/src/net-node.ts index 0e880d4..ee29795 100644 --- a/src/net-node.ts +++ b/src/net-node.ts @@ -5,6 +5,7 @@ import * as dns from "node:dns"; import * as fs from "node:fs"; import * as net from "node:net"; import * as os from "node:os"; +import * as path from "node:path"; import { DGRAM_RCVBUF, HIGH_WATER, MAX_DGRAM, Sock } from "./sockets.ts"; import type { Endpoint, NetBackend, NetIf } from "./sockets.ts"; import * as E from "./sockets.ts"; @@ -57,6 +58,8 @@ interface TcpImpl { interface UdpImpl { ds?: dgram.Socket; + // the address the socket was bound to before a connect gave it a source address + boundLocal?: Endpoint; // sends the host socket has not finished, which a close must wait for pending?: number; closing?: boolean; @@ -70,6 +73,14 @@ function reset(ns: net.Socket) { else ns.destroy(); } +// A socket address holds 107 bytes of path; a long absolute path under the working +// directory goes in relative, as a program that binds a relative name does. +function shortUnixPath(p: string): string { + if (Buffer.byteLength(p) <= 107) return p; + const rel = path.relative(process.cwd(), p); + return Buffer.byteLength(rel) < Buffer.byteLength(p) ? rel : p; +} + export class NodeNet implements NetBackend { private live = new Set(); @@ -129,15 +140,29 @@ export class NodeNet implements NetBackend { readonly hostUnix = true; + // The names clients of this VM bound before connecting to a path, in connect order. A host + // socket cannot tell its peer's name, so an accepted socket takes the next one. + private unixClients = new Map(); + bindUnix(s: Sock, path: string) { - return this.listenOn(s, { path }, () => { + return this.listenOn(s, { path: shortUnixPath(path) }, () => { s.localPath = path; - }); + }, path); } connectUnix(s: Sock, path: string) { - return this.connectTo(s, { path }, () => { + const entry = { name: s.localPath }; + const names = this.unixClients.get(path); + if (names) names.push(entry); + else this.unixClients.set(path, [entry]); + return this.connectTo(s, { path: shortUnixPath(path) }, () => { s.remotePath = path; + s.peerInVm = [...this.live].some((l) => l.localPath === path && (l.impl as TcpImpl | null)?.srv); + }, () => { + const list = this.unixClients.get(path); + const i = list?.indexOf(entry) ?? -1; + if (i >= 0) list!.splice(i, 1); + if (list?.length === 0) this.unixClients.delete(path); }); } @@ -148,7 +173,7 @@ export class NodeNet implements NetBackend { } // Listens with the given net options; s collects what connects. - private listenOn(s: Sock, options: net.ListenOptions, bound: (srv: net.Server) => void): Promise { + private listenOn(s: Sock, options: net.ListenOptions, bound: (srv: net.Server) => void, unixPath?: string): Promise { return new Promise((resolve) => { const srv = net.createServer({ allowHalfOpen: true }); srv.once("error", (e) => resolve(errnoOf(e))); @@ -162,6 +187,9 @@ export class NodeNet implements NetBackend { } else { c.unix = true; c.localPath = s.localPath; + const client = unixPath ? this.unixClients.get(unixPath)?.shift() : undefined; + c.remotePath = client?.name ?? ""; + c.peerInVm = client !== undefined; } this.attach(c, ns); s.backlog.push(c); @@ -260,12 +288,15 @@ export class NodeNet implements NetBackend { }); } - private connectTo(s: Sock, options: net.NetConnectOpts, connected: (ns: net.Socket) => void): number { + private connectTo(s: Sock, options: net.NetConnectOpts, connected: (ns: net.Socket) => void, failed?: () => void): number { s.connecting = true; const start = () => { const ns = net.connect({ ...options, allowHalfOpen: true }); this.attach(s, ns); + let made = false; + if (failed) ns.once("close", () => { if (!made) failed(); }); ns.once("connect", () => { + made = true; s.connecting = false; s.connected = true; connected(ns); @@ -285,14 +316,32 @@ export class NodeNet implements NetBackend { const ds = (s.impl as UdpImpl | null)?.ds; if (!ds) return Promise.resolve(E.EBADF); return new Promise((resolve) => { - ds.once("connect", () => { + // Linux lets a connected datagram socket connect again; Node wants it disconnected first + if (s.connected) { + try { ds.disconnect(); } catch { /* not connected */ } + } + const onConnect = () => { s.connected = true; s.remote = { addr: ep.addr.slice(), port: ep.port }; + // connecting gives a wildcard-bound socket the source address the route picks + try { + const a = ds.address(); + const im = s.impl as UdpImpl; + if (a.family === "IPv4" && s.local && E.isWildcard(s.local.addr)) { + im.boundLocal ??= s.local; + s.local = { addr: parseIp(a.address), port: a.port }; + } + } catch { /* keeps the bound address */ } resolve(0); - }); + }; + ds.once("connect", onConnect); try { ds.connect(ep.port, peerString(ep.addr)); } catch (e) { + // the host socket is disconnected by now + ds.removeListener("connect", onConnect); + s.connected = false; + s.remote = null; resolve(errnoOf(e)); } }); @@ -301,7 +350,7 @@ export class NodeNet implements NetBackend { // Linux forgets the port of a socket that disconnects, so bind it again to keep it. async disconnect(s: Sock) { const ds = (s.impl as UdpImpl | null)?.ds; - const local = s.local; + const local = (s.impl as UdpImpl | null)?.boundLocal ?? s.local; s.connected = false; s.remote = null; if (!ds || !local) return 0; diff --git a/src/node.ts b/src/node.ts index 3b3fad7..3a62575 100644 --- a/src/node.ts +++ b/src/node.ts @@ -8,10 +8,11 @@ import * as path from "node:path"; import { deserialize, serialize, setFlagsFromString } from "node:v8"; import { runInNewContext } from "node:vm"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { bootClassPath, bootPatches } from "./bootpath.ts"; import { VFS } from "./fs.ts"; import type { Backend, Stat } from "./fs.ts"; import { Bridge, nativeTable } from "./bridge.ts"; -import { JVM, LM_CLASS, LM_JAR, SnapshotError } from "./jvm.ts"; +import { JVM, LM_CLASS, LM_JAR, LM_MODULE, SnapshotError } from "./jvm.ts"; import type { SnapshotImage, WarmSnapshotImage } from "./snapshot.ts"; import { hostNatives } from "./hostnatives.ts"; import { ImageBackend, imageFd } from "./image.ts"; @@ -318,6 +319,8 @@ export const defaultJdk = path.join(here, "..", "build", "jdk"); export interface NodeRunOptions { classpath: string[]; mainClass?: string; + // -m: "module" or "module/class", resolved from the module path + mainModule?: string; jar?: string; args?: string[]; props?: Record; @@ -358,6 +361,11 @@ export interface NodeRunOptions { warmSnapshot?: boolean; } +// what the java-web command adds +interface LauncherOptions extends NodeRunOptions { + dumpOnQuit?: boolean; +} + function fixedInput(data: Uint8Array) { let done = false; return async () => { @@ -521,7 +529,7 @@ function snapshotCache(dir: string, build: string) { }; } -function nodeVm(o: NodeRunOptions): JVM { +function nodeVm(o: LauncherOptions): JVM { exposeGc(); const vfs = new VFS(); vfs.mount("/", new NodeBackend("/", true)); @@ -567,6 +575,9 @@ function nodeVm(o: NodeRunOptions): JVM { debug: o.debug, assertions: o.assertions && parseAssertions(o.assertions), enablePreview: o.enablePreview, + bootClassPath: bootClassPath(o.props?.["jdk.boot.class.path.append"]), + bootPatch: bootPatches(o.props), + osVersion: os.release(), net: o.net ?? (o.fetch ? undefined : new NodeNet()), fetch: o.fetch, hostName: o.hostName, @@ -614,9 +625,23 @@ function retried(start: () => Promise): Promise { }); } -export function runNode(o: NodeRunOptions): Promise { +export function runNode(o: LauncherOptions): Promise { return retried(() => { const vm = nodeVm(o); + // as HotSpot, a QUIT signal prints every thread's stack and the program goes on + if (o.dumpOnQuit) { + const dump = () => vm.dumpAllStacks(); + process.on("SIGQUIT", dump); + void vm.exited.finally(() => process.off("SIGQUIT", dump)); + // JW_DUMP_STACKS= does the same every so many seconds + const every = Number(process.env.JW_DUMP_STACKS); + if (every > 0) { + const timer = setInterval(dump, every * 1000); + timer.unref(); + void vm.exited.finally(() => clearInterval(timer)); + } + } + if (o.mainModule) return vm.run(LM_MODULE, o.mainModule, o.args ?? []); return o.jar ? vm.run(LM_JAR, path.resolve(o.jar), o.args ?? []) : vm.run(LM_CLASS, o.mainClass!, o.args ?? []); }); } @@ -675,7 +700,7 @@ async function printVersion(o: NodeRunOptions) { // the java-web command, which dist/cli.js runs export function main(argv: string[]) { - const o: NodeRunOptions = { classpath: ["."], props: {} }; + const o: LauncherOptions = { classpath: ["."], props: {}, dumpOnQuit: true }; let i = 0; let version = false; const javaChild = argv[0] === "--java-child"; @@ -723,6 +748,20 @@ export function main(argv: string[]) { } else if (a === "--jdk") o.jdk = argv[++i]; else if (a === "-jar") { o.jar = argv[++i]; i++; break; } + else if (a === "-m" || a === "--module" || a.startsWith("--module=")) { + o.mainModule = a.startsWith("--module=") ? a.slice(9) : argv[++i] ?? ""; + if (!o.mainModule) { + console.error(`Error: ${a.startsWith("--module=") ? "--module=" : a} requires module name`); + process.exit(1); + } + o.props!["jdk.module.main"] = o.mainModule.split("/")[0]; + i++; + break; + } + else if (a.startsWith("-Xbootclasspath/a:")) { + const prev = o.props!["jdk.boot.class.path.append"]; + o.props!["jdk.boot.class.path.append"] = prev ? `${prev}:${a.slice(18)}` : a.slice(18); + } else if (/^(-X|--add-|-verbose|-server|-client)/.test(a)) continue; else if (javaChild && a.startsWith("-")) { console.error(`Unrecognized option: ${a}\nError: Could not create the Java Virtual Machine.\nError: A fatal exception has occurred. Program will exit.`); @@ -739,13 +778,19 @@ export function main(argv: string[]) { }); return; } - if (!o.mainClass && !o.jar) { + if (!o.mainClass && !o.jar && !o.mainModule) { console.error("usage: java-web [-cp path] [-Dkey=value] Main [args] | -jar app.jar [args]"); process.exit(2); } // Collecting first: node's exit teardown can deadlock while it frees very // large array buffers (seen on x64 CI runners with a few hundred MB left). - Promise.resolve().then(() => runNode(o)).then((code) => (globalThis.gc?.(), process.exit(code)), (e) => { + Promise.resolve().then(() => runNode(o)).then(async (code) => { + globalThis.gc?.(); + // output a pipe has not taken yet goes out before the process does + const flushed = [process.stdout, process.stderr].map((s) => new Promise((done) => { s.write("", () => done()); })); + await Promise.race([Promise.all(flushed), new Promise((done) => setTimeout(done, 10000).unref())]); + process.exit(code); + }, (e) => { console.error(`java-web: ${(e as Error).message}`); process.exit(1); }); diff --git a/src/posixtz.ts b/src/posixtz.ts new file mode 100644 index 0000000..5172c22 --- /dev/null +++ b/src/posixtz.ts @@ -0,0 +1,70 @@ +// A POSIX TZ string such as "MEZ-1MESZ,M3.5.0,M10.5.0/3": the UTC offset it +// gives at a time, in minutes east of UTC, as glibc's localtime would. + +const NAME = /^(?:[A-Za-z]{3,}|<[^>]+>)/; +const OFFSET = /^[+-]?\d{1,3}(?::\d{1,2}(?::\d{1,2})?)?/; +const TIME = new RegExp(OFFSET.source + "$"); + +function seconds(s: string): number { + const sign = s[0] === "-" ? -1 : 1; + const [h, m = "0", sec = "0"] = s.replace(/^[+-]/, "").split(":"); + return sign * (Number(h) * 3600 + Number(m) * 60 + Number(sec)); +} + +// Seconds from the start of the year (local wall clock) to a rule's day and time +function ruleSeconds(rule: string, year: number): number | null { + const parts = rule.split("/"); + if (parts.length > 2) return null; + const [day, time = "2"] = parts; + if (!TIME.test(time)) return null; + const at = seconds(time); + const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + let m = /^M(\d+)\.(\d)\.(\d)$/.exec(day); + if (m) { + const [month, week, weekday] = [Number(m[1]), Number(m[2]), Number(m[3])]; + if (month < 1 || month > 12 || week < 1 || week > 5 || weekday > 6) return null; + const first = Date.UTC(year, month - 1, 1); + let d = (weekday - new Date(first).getUTCDay() + 7) % 7 + (week - 1) * 7; + const length = new Date(Date.UTC(year, month, 0)).getUTCDate(); + while (d >= length) d -= 7; + return (first + d * 86400000 - Date.UTC(year, 0, 1)) / 1000 + at; + } + m = /^J(\d+)$/.exec(day); + if (m) { + const n = Number(m[1]); + if (n < 1 || n > 365) return null; + return (n - 1 + (leap && n >= 60 ? 1 : 0)) * 86400 + at; + } + m = /^(\d+)$/.exec(day); + return m && Number(m[1]) <= 365 ? Number(m[1]) * 86400 + at : null; +} + +export function posixOffsetMinutes(tz: string, nowMs: number): number | null { + let rest = tz; + const std = NAME.exec(rest); + if (!std) return null; + rest = rest.slice(std[0].length); + const stdOff = OFFSET.exec(rest); + if (!stdOff) return null; + rest = rest.slice(stdOff[0].length); + const stdEast = -seconds(stdOff[0]); + if (!rest) return stdEast / 60; + const dst = NAME.exec(rest); + if (!dst) return null; + rest = rest.slice(dst[0].length); + const dstOff = OFFSET.exec(rest); + const dstEast = dstOff ? -seconds(dstOff[0]) : stdEast + 3600; + if (dstOff) rest = rest.slice(dstOff[0].length); + // glibc's default rules, the US ones, apply without a rule part + const [startRule, endRule] = rest.startsWith(",") ? rest.slice(1).split(",") : ["M3.2.0", "M11.1.0"]; + if (!endRule) return null; + const year = new Date(nowMs + stdEast * 1000).getUTCFullYear(); + const start = ruleSeconds(startRule, year), end = ruleSeconds(endRule, year); + if (start === null || end === null) return null; + const yearStart = Date.UTC(year, 0, 1) / 1000; + // the start is in standard time, the end in daylight time + const now = nowMs / 1000; + const from = yearStart + start - stdEast, to = yearStart + end - dstEast; + const inDst = from < to ? now >= from && now < to : now >= from || now < to; + return (inDst ? dstEast : stdEast) / 60; +} diff --git a/src/process-node.ts b/src/process-node.ts index 862e778..0d8571a 100644 --- a/src/process-node.ts +++ b/src/process-node.ts @@ -19,6 +19,9 @@ export const ENV_CHANNEL = "JAVA_WEB_ENV"; export const javaChild = (entry: string, jdk: string, packaged: boolean) => [process.execPath, entry, "--java-child", ...(packaged ? [] : ["--jdk", jdk])]; +const ended = (input: Readable) => input.readableEnded || input.destroyed + ? Promise.resolve() + : new Promise((resolve) => { input.once("end", resolve); input.once("close", resolve); input.once("error", resolve); }); const drained = (output: Writable) => output.writableNeedDrain ? new Promise((resolve) => { output.once("drain", resolve); output.once("close", resolve); }) : undefined; @@ -45,6 +48,11 @@ function findProgram(command: string, cwd: string, parentPath: string | undefine return Object.assign(new Error(denied ? "EACCES" : "ENOENT"), { code: denied ? "EACCES" : "ENOENT", errno: denied ? -13 : -2 }); } +// A child's output waits in memory up to this many bytes before the child is held back. More than a +// pipe holds, since a java-web child cannot close its host stdout: a parent that reads stdout to its +// end before stderr would otherwise wait on a child held back on stderr. +const HELD = 1 << 24; + class Pipe implements ProcessPipe { private chunks: Uint8Array[] = []; private length = 0; @@ -54,6 +62,7 @@ class Pipe implements ProcessPipe { private output: Writable | null; private forwarding = false; private wake: (() => void)[] = []; + private limit = HELD; constructor(input: Readable | null, output: Writable | null) { this.output = output; if (input) this.attach(input); @@ -65,7 +74,7 @@ class Pipe implements ProcessPipe { input.on("data", (chunk: Buffer) => { this.chunks.push(new Uint8Array(chunk)); this.length += chunk.length; - if (this.length >= 65536) for (const stream of this.inputs) stream.pause(); + if (this.length >= this.limit) for (const stream of this.inputs) stream.pause(); this.notify(); }); let done = false; @@ -79,6 +88,11 @@ class Pipe implements ProcessPipe { input.once("error", end); } private notify() { for (const wake of this.wake.splice(0)) wake(); } + // While a child that exited has its output read, takes in what its pipe still holds + hold(on: boolean) { + this.limit = on ? HELD : HELD + (1 << 20); + for (const stream of this.inputs) on && this.length >= HELD ? stream.pause() : stream.resume(); + } async read(dst: Uint8Array, off: number, len: number): Promise { while (!this.length && !this.ended) await new Promise((resolve) => this.wake.push(resolve)); if (!this.length) return -1; @@ -93,7 +107,7 @@ class Pipe implements ProcessPipe { copied += take; } this.length -= n; - if (this.length < 32768) for (const stream of this.inputs) stream.resume(); + if (this.length < HELD / 2) for (const stream of this.inputs) stream.resume(); return n; } write(bytes: Uint8Array): Promise { @@ -249,13 +263,28 @@ export function processNatives(entry: string, jdk: string, packaged: boolean, ho merged?.writer.destroy(); const status = (code: number | null, signal: NodeJS.Signals | null) => code ?? 128 + ((os.constants.signals as Record)[signal ?? ""] ?? 0); - const exited = new Promise((resolve) => child.once("exit", (code, signal) => resolve(status(code, signal)))); + // The JDK takes what is left in a child's pipes when it exits, so that output is read before + // the VM hears of the exit. A grandchild can keep a pipe open, so this waits only a moment. + const pipes: Pipe[] = []; + const outputs = [merged ? merged.reader : child.stdout, merged ? null : child.stderr].filter((s): s is Readable => s !== null); + const exited = new Promise((resolve) => { + child.once("exit", (code, signal) => { + for (const pipe of pipes) pipe.hold(false); + const done = () => { + for (const pipe of pipes) pipe.hold(true); + resolve(status(code, signal)); + }; + setTimeout(done, 250); + void Promise.all(outputs.map(ended)).then(done); + }); + }); const created: number[] = []; for (let i = 0; i < 3; i++) { if (i === 2 && redirectError) { descriptors[i] = -1; } else if (descriptors[i] < 0) { const pipe = new Pipe(i === 0 ? null : i === 1 && merged ? merged.reader : child.stdio[i] as Readable, i === 0 ? child.stdio[i] as Writable : null); + if (i > 0) pipes.push(pipe); descriptors[i] = addProcessPipe(t, pipe); created.push(descriptors[i]); } else { diff --git a/src/snapshot.ts b/src/snapshot.ts index 0204d86..3b1eded 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -17,7 +17,7 @@ import { Frame, JThread, NativeFrame, STATUS_RUNNABLE } from "./thread.ts"; import type { NativeGen } from "./thread.ts"; // Bump when the layout of an image changes; images of another format are ignored. -export const SNAPSHOT_FORMAT = 2; +export const SNAPSHOT_FORMAT = 3; export interface SnapshotImage { format: number; @@ -62,6 +62,8 @@ interface VmRec { boot: unknown; bootPackages: unknown; bootModules: unknown; + bootModulePackages: unknown; + bootAppendPackages: unknown; bootUnnamedModule: unknown; interned: unknown; hashSeed: number; @@ -433,6 +435,8 @@ export function dumpVM(vm: JVM, main: JThread, key: string, library = false): Sn boot: e.enc(vm.boot, "boot"), bootPackages: e.enc(vm.bootPackages, "bootPackages"), bootModules: e.enc(vm.bootModules, "bootModules"), + bootModulePackages: e.enc(vm.bootModulePackages, "bootModulePackages"), + bootAppendPackages: e.enc(vm.bootAppendPackages, "bootAppendPackages"), bootUnnamedModule: e.enc(vm.bootUnnamedModule, "bootUnnamedModule"), interned: e.enc((vm as any).interned, "interned"), hashSeed: (vm as any).hashSeed, @@ -551,6 +555,8 @@ export function restoreVM(vm: JVM, image: SnapshotImage, main: JThread) { for (const [k, c] of boot) vm.boot.set(k, c); for (const [k, p] of d.now(v.bootPackages) as Map) vm.bootPackages.set(k, p); for (const [k, m] of d.now(v.bootModules) as Map) vm.bootModules.set(k, m); + for (const [k, m] of d.now(v.bootModulePackages) as Map) vm.bootModulePackages.set(k, m); + for (const [k, p] of d.now(v.bootAppendPackages) as Map) vm.bootAppendPackages.set(k, p); vm.bootUnnamedModule = d.dec(v.bootUnnamedModule); const interned = (vm as any).interned as Map; for (const [k, s] of d.now(v.interned) as Map) interned.set(k, s); diff --git a/src/sockets.ts b/src/sockets.ts index 95fcd7a..7e20493 100644 --- a/src/sockets.ts +++ b/src/sockets.ts @@ -129,6 +129,8 @@ export class Sock { pipe = false; // the in-VM peer of a loopback stream or pipe peer: Sock | null = null; + // a host Unix socket whose other end is in this VM + peerInVm = false; // multicast groups joined, by address groups = new Set(); // backend state @@ -166,7 +168,8 @@ export class Sock { if (this.connecting) return r; if (!this.connected) return r | POLLOUT | POLLHUP; if (this.rxBytes > 0 || this.eof || (this.shutRd && !this.pipe)) r |= POLLIN; - if (!this.shutWr && !this.wblocked) r |= POLLOUT; + // after SHUT_WR a write fails at once, so Linux reports the socket writable + if (this.shutWr ? !this.pipe : !this.wblocked) r |= POLLOUT; if (this.pipe && this.shutRd && !this.peer) r |= POLLERR; if ((this.eof || this.shutRd) && this.shutWr) r |= POLLHUP; return r; @@ -593,6 +596,7 @@ export class LoopbackNet implements NetBackend { const b = new Sock("stream"); b.unix = true; b.localPath = l.localPath; + b.remotePath = s.localPath; b.connected = true; b.peer = s; s.peer = b; diff --git a/test/java/ConstantPoolErrors.java b/test/java/ConstantPoolErrors.java new file mode 100644 index 0000000..3177252 --- /dev/null +++ b/test/java/ConstantPoolErrors.java @@ -0,0 +1,30 @@ +// javac: --add-exports java.base/jdk.internal.access=ALL-UNNAMED --add-exports java.base/jdk.internal.reflect=ALL-UNNAMED +// java: --add-exports java.base/jdk.internal.access=ALL-UNNAMED --add-exports java.base/jdk.internal.reflect=ALL-UNNAMED +import java.util.concurrent.Callable; +import jdk.internal.access.SharedSecrets; +import jdk.internal.reflect.ConstantPool; + +public class ConstantPoolErrors { + static void check(String what, Callable c) { + try { + System.out.println(what + " -> " + c.call()); + } catch (Exception e) { + System.out.println(what + " -> " + e); + } + } + + public static void main(String[] args) { + ConstantPool cp = SharedSecrets.getJavaLangAccess().getConstantPool(ConstantPoolErrors.class); + int size = cp.getSize(); + check("utf8 at 0", () -> cp.getUTF8At(0)); + check("utf8 at -1", () -> cp.getUTF8At(-1)); + check("int at size", () -> cp.getIntAt(size)); + check("long at 1", () -> cp.getLongAt(1)); + check("string at 1", () -> cp.getStringAt(1)); + check("class at 1", () -> cp.getClassAt(1)); + check("member at 1", () -> java.util.Arrays.toString(cp.getMemberRefInfoAt(1))); + check("name and type at 1", () -> cp.getNameAndTypeRefInfoAt(1)); + check("tag at size", () -> cp.getTagAt(size)); + check("tag at 0", () -> cp.getTagAt(0)); + } +} diff --git a/test/java/MemoryManagers.java b/test/java/MemoryManagers.java new file mode 100644 index 0000000..ae2a50d --- /dev/null +++ b/test/java/MemoryManagers.java @@ -0,0 +1,25 @@ +// java: -XX:+UseG1GC +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryManagerMXBean; +import java.lang.management.MemoryPoolMXBean; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class MemoryManagers { + public static void main(String[] args) { + // the order of the platform's managers depends on the JDK's hashing, so the lines are sorted + List lines = new ArrayList<>(); + for (MemoryPoolMXBean p : ManagementFactory.getMemoryPoolMXBeans()) { + lines.add("pool " + p.getName() + " " + p.getType() + " " + p.isUsageThresholdSupported() + + " " + p.isCollectionUsageThresholdSupported() + " " + Arrays.toString(p.getMemoryManagerNames())); + } + for (MemoryManagerMXBean m : ManagementFactory.getMemoryManagerMXBeans()) { + lines.add("manager " + m.getName() + " " + Arrays.toString(m.getMemoryPoolNames()) + " " + (m instanceof GarbageCollectorMXBean)); + } + Collections.sort(lines); + lines.forEach(System.out::println); + } +} diff --git a/test/java/OsVersion.java b/test/java/OsVersion.java new file mode 100644 index 0000000..c52a299 --- /dev/null +++ b/test/java/OsVersion.java @@ -0,0 +1,7 @@ +public class OsVersion { + public static void main(String[] args) throws Exception { + System.out.println(System.getProperty("os.name") + " " + System.getProperty("os.version")); + Process p = new ProcessBuilder("uname", "-r").start(); + System.out.println(System.getProperty("os.version").equals(new String(p.getInputStream().readAllBytes()).trim())); + } +} diff --git a/test/java/Processes.java b/test/java/Processes.java index 2782eb2..cfbfff8 100644 --- a/test/java/Processes.java +++ b/test/java/Processes.java @@ -77,6 +77,12 @@ public static void main(String[] args) throws Exception { System.out.println("merged=" + (both.contains("out") && both.contains("err")) + ":" + merged.waitFor()); Process ordered = new ProcessBuilder("sh", "-c", "i=0; while [ $i -lt 100 ]; do printf a; printf b >&2; i=$((i+1)); done").redirectErrorStream(true).start(); System.out.println("order=" + output(ordered).equals("ab".repeat(100)) + ":" + ordered.waitFor()); + // output that fits in the pipe is still there once the child has exited + for (boolean m : new boolean[] {false, true}) { + Process late = new ProcessBuilder("sh", "-c", "head -c 60000 /dev/zero").redirectErrorStream(m).start(); + int code = late.waitFor(); + System.out.println("after exit=" + late.getInputStream().readAllBytes().length + ":" + code); + } Process redirect = new ProcessBuilder("echo", "file").redirectOutput(file.toFile()).start(); System.out.println("redirect=" + redirect.waitFor() + ":" + Files.readString(file).trim()); Process append = new ProcessBuilder("echo", "extra").redirectOutput(ProcessBuilder.Redirect.appendTo(file.toFile())).start(); diff --git a/test/java/UnixSockets.java b/test/java/UnixSockets.java index dbf80c8..4f062f6 100644 --- a/test/java/UnixSockets.java +++ b/test/java/UnixSockets.java @@ -70,6 +70,7 @@ public static void main(String[] args) throws Exception { } pipe.source().close(); paths(); + clientNames(); } // A path names a file: a deleted one can be bound again, and equal paths reach the same socket. @@ -105,4 +106,29 @@ static void paths() throws Exception { } Files.deleteIfExists(relative); } + + // An accepted socket reports the name its client bound, even after a failed connect to the path. + static void clientNames() throws Exception { + Path dir = Files.createTempDirectory("uds"); + var address = UnixDomainSocketAddress.of(dir.resolve("s.sock")); + try (SocketChannel c = SocketChannel.open(StandardProtocolFamily.UNIX)) { + c.bind(UnixDomainSocketAddress.of(dir.resolve("first.sock"))); + c.connect(address); + } catch (IOException e) { + System.out.println("first " + e.getClass().getSimpleName()); + } + try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + SocketChannel c = SocketChannel.open(StandardProtocolFamily.UNIX)) { + server.bind(address); + c.bind(UnixDomainSocketAddress.of(dir.resolve("second.sock"))); + c.connect(address); + try (SocketChannel a = server.accept()) { + System.out.println("accepted from " + ((UnixDomainSocketAddress) a.getRemoteAddress()).getPath().getFileName()); + } + } + try (var files = Files.list(dir)) { + for (Path f : files.toList()) Files.delete(f); + } + Files.delete(dir); + } } diff --git a/test/jtreg/passing.txt b/test/jtreg/passing.txt index 1998b6b..3a64774 100644 --- a/test/jtreg/passing.txt +++ b/test/jtreg/passing.txt @@ -3996,3 +3996,37 @@ java/nio/file/Files/SBC.java java/util/zip/ZipFile/CenSizeTooLarge.java java/util/zip/ZipFile/EndOfCenValidation.java jdk/nio/zipfs/EndOfCenValidation.java +java/lang/Class/forName/modules/TestDriver.java +java/lang/ClassLoader/GetSystemPackage.java +java/lang/ClassLoader/getResource/GetResource.java +java/lang/ModuleTests/access/AccessTest.java +java/lang/ProcessBuilder/SkipTest.java +java/lang/ScopedValue/StressStackOverflow.java +java/lang/ScopedValue/StressStackOverflow.java#2 +java/lang/ScopedValue/StressStackOverflow.java#3 +java/lang/ScopedValue/StressStackOverflow.java#4 +java/lang/System/LoggerFinder/modules/JDKLoggerForJDKTest.java +java/lang/System/LoggerFinder/modules/NamedLoggerForJDKTest.java +java/lang/System/LoggerFinder/modules/UnnamedLoggerForJDKTest.java +java/lang/System/OsVersionTest.java +java/lang/annotation/typeAnnotations/BadCPIndex.java +java/lang/invoke/DumpMethodHandleInternals.java +java/lang/invoke/lambda/LambdaAsm.java +java/lang/invoke/lambda/LogGeneratedClassesTest.java +java/lang/management/MemoryMXBean/MemoryTest.java#5 +java/lang/management/OperatingSystemMXBean/GetSystemLoadAverage.java +java/lang/reflect/Proxy/ProxyClassAccessTest.java +java/lang/reflect/Proxy/ProxyModuleMapping.java +java/lang/reflect/Proxy/ProxyTest.java +java/net/Socket/AsyncShutdown.java +java/net/SocketOption/TcpKeepAliveTest.java +java/nio/MappedByteBuffer/MapSyncFail.java +java/nio/channels/DatagramChannel/AdaptorConnect.java +java/nio/channels/DatagramChannel/Sender.java +java/nio/channels/unixdomain/Bind.java +java/nio/channels/unixdomain/SocketOptions.java +java/util/ResourceBundle/modules/basic/BasicTest.java +java/util/ResourceBundle/modules/cache/CacheTest.java +java/util/ResourceBundle/modules/casesensitive/CaseInsensitiveNameClash.java +java/util/ResourceBundle/modules/visibility/VisibilityTest.java +java/util/TimeZone/CustomTzIDCheckDST.java diff --git a/test/jtreg/times.tsv b/test/jtreg/times.tsv index c96d4d0..b62ced2 100644 --- a/test/jtreg/times.tsv +++ b/test/jtreg/times.tsv @@ -58,6 +58,7 @@ 94523 java/util/Calendar/CalendarTestScripts/JapaneseRollTests.java 94008 java/net/httpclient/http2/ConnectionFlowControlTest.java 93744 java/net/httpclient/ThrowingSubscribersSanity.java +93697 java/util/ResourceBundle/modules/basic/BasicTest.java 93525 java/util/zip/DeInflate.java 93198 java/lang/Long/ToString.java 92855 java/lang/System/LoggerFinder/internal/backend/LoggerFinderBackendTest.java @@ -118,6 +119,7 @@ 68035 java/time/test/java/time/format/TestUnicodeExtension.java 67944 java/lang/System/LoggerFinder/internal/BootstrapLogger/BootstrapLoggerTest.java 67847 java/net/httpclient/HttpClientAuthRetryLimitTest.java +67392 java/util/ResourceBundle/modules/visibility/VisibilityTest.java 67326 java/security/KeyStore/TestKeyStoreBasic.java 65745 java/net/Socket/asyncClose/AsyncClose.java 65693 java/security/MessageDigest/ThreadSafetyTest.java @@ -129,7 +131,11 @@ 63402 java/beans/Performance/Test4058433.java 63310 java/net/Socket/Timeouts.java 63072 java/nio/channels/FileChannel/CleanerTest.java +62928 java/lang/ScopedValue/StressStackOverflow.java#2 62820 java/lang/invoke/VarHandles/VarHandleTestMethodTypeFloat.java +62650 java/lang/ScopedValue/StressStackOverflow.java#4 +62535 java/lang/ScopedValue/StressStackOverflow.java#3 +62481 java/lang/ScopedValue/StressStackOverflow.java 60333 java/util/TimeZone/Bug8149452.java 60194 java/lang/ProcessBuilder/Basic.java#3 59689 java/security/cert/CertPathValidator/OCSP/OCSPTimeout.java @@ -177,6 +183,7 @@ 48574 java/lang/ClassLoader/getResource/modules/ResourcesTest.java 48410 java/net/httpclient/NoBodyPartOne.java 48315 java/util/concurrent/ConcurrentQueues/ConcurrentQueueLoops.java +48147 java/lang/ClassLoader/getResource/GetResource.java 48118 java/net/httpclient/SplitResponseSSLKeepAliveAsync.java 47953 java/net/httpclient/FlowAdapterSubscriberTest.java 47612 java/text/Format/ListFormat/TestListFormat.java @@ -261,7 +268,6 @@ 29918 java/awt/geom/Path2D/Path2DGrow.java 29810 java/io/Serializable/records/SerialPersistentFieldsTest.java 29515 java/nio/charset/coders/BashStreams.java -29451 java/lang/System/LoggerFinder/modules/LoggerInImageTest.java 29314 java/net/httpclient/RedirectTimeoutTest.java 29301 java/net/httpclient/SplitResponseAsync.java 29246 java/lang/invoke/VarHandles/VarHandleTestByteArrayAsChar.java @@ -273,7 +279,6 @@ 28950 java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java#3 28778 java/time/tck/java/time/TCKLocalDate.java 28753 java/security/misc/TestDefaultRandom.java -28642 java/lang/System/LoggerFinder/modules/JDKLoggerForImageTest.java 28540 javax/crypto/CipherSpi/CipherByteBufferOverwriteTest.java 28495 java/io/Serializable/failureAtomicity/FailureAtomicity.java 28339 java/net/InetAddress/HostsFileOrderingTest.java @@ -282,6 +287,7 @@ 28213 java/security/Signature/SignatureGetInstance.java 28119 java/net/httpclient/websocket/PendingTextPingClose.java 28060 java/lang/invoke/VarHandles/LazyInitializingTest.java +27650 java/lang/reflect/Proxy/ProxyTest.java 27517 java/util/Calendar/SupplementalJapaneseEraTestRun.java 27487 java/security/SignedObject/Correctness.java 27422 java/lang/Double/ParseDouble.java @@ -289,7 +295,6 @@ 26897 java/nio/channels/SocketChannel/AdaptorStreams.java 26886 java/text/BreakIterator/BreakIteratorTest.java 26853 java/nio/channels/FileChannel/ClosedByInterrupt.java -26828 java/lang/System/LoggerFinder/modules/NamedLoggerForImageTest.java 26649 java/lang/Thread/virtual/stress/NotifiedThenTimedOutWait.java#2 26581 java/lang/Character/CheckProp.java 26478 java/nio/channels/Selector/ChangingInterests.java @@ -298,7 +303,6 @@ 26329 java/text/Format/DateFormat/SimpleDateFormatPatternTest.java 26157 java/lang/ProcessHandle/OnExitTest.java 26112 java/lang/Thread/virtual/stress/GetStackTraceALotWhenBlocking.java -26064 java/lang/System/LoggerFinder/modules/UnnamedLoggerForImageTest.java 25951 java/lang/invoke/lambda/LambdaFileEncodingSerialization.java 25826 java/util/Arrays/largeMemory/ParallelPrefix.java 25808 java/lang/invoke/ClassValueTest.java @@ -333,6 +337,7 @@ 23148 java/util/Arrays/CopyMethods.java 23144 java/net/httpclient/HttpsTunnelTest.java 22927 java/net/DatagramSocket/SendReceiveMaxSize.java +22902 java/lang/reflect/Proxy/ProxyClassAccessTest.java 22788 java/util/BitSet/BSMethods.java 22720 java/beans/XMLEncoder/javax_swing_JTree.java 22505 java/lang/String/LiteralReplace.java @@ -345,6 +350,7 @@ 22095 java/text/Format/NumberFormat/CurrencyFormat.java 22081 java/util/concurrent/StructuredTaskScope/StressCancellation.java 22047 java/text/Format/CompactNumberFormat/TestFormatToCharacterIterator.java +21905 java/util/ResourceBundle/modules/cache/CacheTest.java 21801 javax/crypto/CryptoPermissions/TestUnlimited.java 21721 java/time/tck/java/time/TCKZoneOffset.java 21680 java/net/httpclient/HttpResponseConnectionLabelTest.java @@ -445,6 +451,7 @@ 17075 java/util/jar/Manifest/ValueUtf8Coding.java 17022 java/lang/String/concat/ImplicitStringConcatShapes.java 17019 java/net/Authenticator/B8034170.java +16990 java/lang/ClassLoader/GetSystemPackage.java 16936 java/util/Date/DateGregorianCalendarTest.java 16910 java/lang/invoke/VarHandles/VarHandleTestAccessBoolean.java 16901 java/util/Scanner/ScanTest.java @@ -479,11 +486,13 @@ 16032 java/lang/ClassLoader/getResource/automaticmodules/Driver.java 16017 java/lang/String/CompactString/GetChars.java 15982 java/time/tck/java/time/format/TCKOffsetPrinterParser.java +15954 java/lang/System/LoggerFinder/modules/NamedLoggerForJDKTest.java 15943 java/io/Serializable/records/BadCanonicalCtrTest.java 15941 java/net/DatagramSocket/SetGetSendBufferSize.java 15916 java/net/MulticastSocket/SetLoopbackOption.java 15887 java/security/KeyPairGenerator/GenerateRSAKeyPair.java 15852 java/lang/String/CompactString/Replace.java +15828 java/lang/System/LoggerFinder/modules/UnnamedLoggerForJDKTest.java 15733 java/util/ServiceLoader/NoInterferenceTest.java 15733 java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java 15671 java/util/Calendar/CalendarDisplayNamesTest.java @@ -497,6 +506,7 @@ 15483 java/net/httpclient/HttpResponseLimitingTest.java 15430 java/net/httpclient/DebugLoggerTest.java 15391 java/io/Serializable/serialFilter/SerialFactoryExample.java +15381 java/lang/invoke/lambda/LogGeneratedClassesTest.java 15377 java/lang/String/CompactString/OffsetByCodePoints.java 15344 java/text/Format/CompactNumberFormat/TestCNFRounding.java 15316 java/security/KeyPairGenerator/Failover.java @@ -508,6 +518,7 @@ 15079 java/security/KeyAgreement/NegativeTest.java 15076 java/io/Serializable/serialFilter/MixedFiltersTest.java 15072 java/util/concurrent/atomic/DoubleAdderDemo.java +15044 java/lang/System/LoggerFinder/modules/JDKLoggerForJDKTest.java 15028 java/util/IdentityHashMap/Capacity.java 15019 java/lang/Character/CharacterName.java 15015 java/lang/ClassLoader/GetDotResource.java @@ -554,6 +565,7 @@ 13956 java/lang/invoke/lambda/LambdaStackTrace.java 13940 java/util/jar/JarInputStream/ExtraFileInMetaInf.java 13932 java/beans/PropertyEditor/TestShortClassNull.java +13889 java/net/Socket/AsyncShutdown.java 13827 java/net/httpclient/CancelledResponse2.java 13767 java/io/Serializable/unshared/Write.java 13748 java/beans/XMLEncoder/javax_swing_JButton.java @@ -589,7 +601,6 @@ 13237 java/io/Serializable/arraySuidConflict/Write.java 13230 java/lang/StableValue/StableValueFactoriesTest.java 13228 java/beans/PropertyEditor/TestLongTypeNull.java -13208 java/util/ResourceBundle/modules/unnamed/UnNamedTest.java 13207 java/security/KeyStore/ProbeKeystores.java 13143 java/text/Format/CompactNumberFormat/TestCompactPatternsValidity.java 13140 java/net/URLClassLoader/definePackage/SplitPackage.java @@ -673,7 +684,6 @@ 11776 java/lang/invoke/LoopCombinatorTest.java 11764 java/time/tck/java/time/chrono/TCKIsoChronology.java 11758 java/awt/geom/Path2D/NonFiniteTests.java -11741 java/util/ResourceBundle/modules/layer/LayerTest.java 11731 java/io/Serializable/enum/missingConstant/Write.java 11718 java/nio/channels/DatagramChannel/AdaptorAsyncCloseAfterReceive.java 11718 java/io/Serializable/records/SerialVersionUIDTest.java @@ -777,6 +787,7 @@ 10249 java/beans/XMLEncoder/javax_swing_BoxLayout.java 10232 java/net/httpclient/PlainProxyConnectionTest.java 10227 java/util/concurrent/Future/DefaultMethods.java +10178 java/lang/System/LoggerFinder/modules/JDKLoggerForImageTest.java 10171 java/io/File/GetAbsolutePath.java 10140 java/beans/XMLEncoder/java_sql_Timestamp.java 10139 java/text/Format/NumberFormat/Bug6278616.java @@ -792,6 +803,7 @@ 9978 java/util/TimeZone/Bug6329116.java 9965 java/text/Format/DateFormat/Bug6530336.java 9956 java/util/Currency/CNPGetDisplayName.java +9951 java/lang/System/LoggerFinder/modules/LoggerInImageTest.java 9914 java/util/Collections/FindSubList.java 9912 java/util/Calendar/CalendarLeapYearAddTest.java 9911 java/beans/XMLEncoder/Test6437265.java @@ -813,9 +825,11 @@ 9654 java/util/List/NestedSubList.java 9654 java/text/Collator/KoreanTest.java 9642 java/awt/geom/Path2D/FillPPathTest.java +9629 java/lang/System/LoggerFinder/modules/UnnamedLoggerForImageTest.java 9629 java/util/Calendar/CalendarLeapYearRollTest.java 9610 java/lang/IntegralPrimitiveToString.java 9609 java/nio/file/WatchService/DeleteInterference.java +9603 java/lang/System/LoggerFinder/modules/NamedLoggerForImageTest.java 9603 java/text/Collator/RuleBasedCollatorTest.java 9602 java/util/jar/JarFile/VerifySignedJar.java 9600 java/lang/Thread/virtual/CarrierThreadInfo.java#4 @@ -876,8 +890,10 @@ 8976 java/lang/String/concat/ImplicitStringConcat.java 8973 java/util/BitSet/ImmutableBitSet.java 8965 java/time/tck/java/time/format/TCKDateTimeFormatters.java +8946 java/util/ResourceBundle/modules/layer/LayerTest.java 8920 java/net/Socket/setReuseAddress/Restart.java 8903 jdk/nio/zipfs/CopyMoveTests.java +8898 java/util/ResourceBundle/modules/casesensitive/CaseInsensitiveNameClash.java 8896 java/util/Calendar/bug4409072.java 8892 java/nio/channels/etc/MemorySegments.java 8889 java/util/Locale/ExpectedAdapterTypes.java @@ -891,6 +907,7 @@ 8848 java/net/SocketInputStream/SocketTimeout.java 8837 java/lang/String/CompactString/CharAt.java 8805 java/util/TimeZone/TimeZoneTest.java +8793 java/util/ResourceBundle/modules/unnamed/UnNamedTest.java 8782 java/util/zip/ZipFile/ReadAfterClose.java 8766 java/util/Locale/LegacyCodesClassInvariant.java 8763 java/security/SecureRandom/Serialize.java @@ -959,6 +976,7 @@ 8319 java/util/Collections/CheckedMapBash.java 8318 java/util/Map/BasicSerialization.java 8309 java/net/httpclient/HttpRequestNewBuilderTest.java +8303 java/lang/Class/forName/modules/TestDriver.java 8299 java/lang/StrictMath/Atan2Tests.java 8284 java/util/Calendar/ResolutionTest.java 8257 java/time/tck/java/time/chrono/serial/TCKChronoLocalDateSerialization.java @@ -978,6 +996,7 @@ 8130 java/lang/String/CompactString/Equals.java 8130 java/beans/PropertyEditor/TestByteTypeNull.java 8122 java/beans/Introspector/8159696/UnloadClassBeanInfo.java +8112 java/util/TimeZone/CustomTzIDCheckDST.java 8111 java/io/Serializable/serialver/nested/NestedTest.java 8107 java/io/Serializable/records/BasicRecordSer.java 8100 java/util/Calendar/NonLenientTest.java @@ -1112,6 +1131,7 @@ 7293 java/util/Calendar/bug4514831.java 7292 java/util/Properties/InitialCapacity.java 7284 java/lang/management/ThreadMXBean/ThreadInfoArray.java +7283 java/lang/ModuleTests/access/AccessTest.java 7278 java/beans/PropertyEditor/TestEnumClassJava.java 7277 javax/crypto/NullCipher/TestNPE.java 7276 java/beans/PropertyEditor/TestShortTypeJava.java @@ -1212,6 +1232,7 @@ 6797 java/net/httpclient/http2/ImplicitPushCancel.java 6785 java/lang/invoke/VarHandles/accessibility/TestFieldLookupAccessibility.java 6779 java/text/Format/DecimalFormat/EqualityTest.java +6777 java/lang/invoke/lambda/LambdaAsm.java 6772 java/time/tck/java/time/format/TCKDateTimeTextPrinting.java 6760 java/nio/channels/unixdomain/IOExchanges.java 6754 java/awt/geom/Path2D/GetBounds2DPrecisionTest.java @@ -1243,6 +1264,7 @@ 6640 java/text/Collator/EnglishTest.java 6633 java/util/RandomAccess/Basic.java 6631 java/nio/channels/AsynchronousSocketChannel/CompletionHandlerRelease.java +6631 java/nio/channels/DatagramChannel/AdaptorConnect.java 6630 java/security/cert/CertPathValidator/nameConstraintsRFC822/ValidateCertPath.java 6626 java/nio/channels/etc/LocalSocketAddressType.java 6626 java/lang/StringBuffer/Comparison.java @@ -1390,6 +1412,7 @@ 6017 java/beans/XMLEncoder/java_util_Collections_SynchronizedList.java 6010 java/text/Format/CompactNumberFormat/TestPlurals.java 6009 java/io/InputStreamReader/StatefulDecoderNearEOF.java +6008 java/nio/MappedByteBuffer/MapSyncFail.java 5998 java/io/Reader/ReadIntoZeroLengthArray.java 5987 java/net/DatagramSocket/DatagramSocketExample.java 5987 java/lang/invoke/defineHiddenClass/TypeDescriptorTest.java @@ -1532,6 +1555,7 @@ 5490 javax/xml/jaxp/XPath/8015978/XPathNegativeZero.java 5483 java/time/tck/java/time/format/TCKSignStyle.java 5477 java/util/Arrays/SetAllTest.java +5476 java/lang/annotation/typeAnnotations/BadCPIndex.java 5474 java/nio/channels/FileLock/Overlaps.java 5472 java/util/Properties/ConcurrentLoadAndStoreXML.java 5472 java/text/Format/DateFormat/DateFormatRoundTripTest.java @@ -1965,6 +1989,7 @@ 4057 java/lang/invoke/LambdaFormTest.java 4052 java/lang/Long/BitTwiddle.java 4052 java/beans/Introspector/Test6963811.java +4052 java/nio/channels/unixdomain/Bind.java 4049 java/io/Reader/TransferTo.java 4048 java/io/Serializable/writeReplace/WriteReplace.java 4048 java/io/BufferedReader/ReadLineSync.java @@ -2056,6 +2081,7 @@ 3862 java/beans/XMLEncoder/java_awt_MenuShortcut.java 3860 java/util/ResourceBundle/Test4300693.java 3859 java/beans/XMLEncoder/java_lang_reflect_Method.java +3859 java/nio/channels/unixdomain/SocketOptions.java 3858 java/time/tck/java/time/MockSimplePeriod.java 3857 java/util/logging/HigherResolutionTimeStamps/LogRecordWithNanosAPI.java 3852 java/beans/XMLEncoder/javax_swing_border_EtchedBorder.java @@ -2126,6 +2152,7 @@ 3754 java/beans/Introspector/Test4948761.java 3752 javax/crypto/CryptoPermissions/CryptoPolicyFallback.java 3752 java/beans/XMLEncoder/java_awt_Cursor.java +3751 java/nio/channels/DatagramChannel/Sender.java 3750 java/io/OutputStreamWriter/Encode.java 3749 java/lang/annotation/typeAnnotations/ConstructorReceiverTest.java 3748 java/io/FilePermission/Correctness.java @@ -2320,6 +2347,7 @@ 3489 java/net/MulticastSocket/MulticastAddresses.java 3487 java/lang/invoke/FilterArgumentsTest.java 3486 java/net/Socket/LinkLocal.java +3485 java/lang/System/OsVersionTest.java 3482 java/util/concurrent/TimeUnit/Basic.java 3481 java/io/OutputStream/WriteParams.java 3480 java/net/InetSocketAddress/B6469803.java @@ -2966,6 +2994,7 @@ 2687 java/io/charStreams/StringConvert.java 2686 java/util/EnumSet/LargeEnumIteratorRemoveResilience.java 2684 java/io/Serializable/classDescFlagConflict/Read.java +2684 java/lang/ProcessBuilder/SkipTest.java 2680 java/io/RandomAccessFile/Seek.java 2677 java/util/zip/ZipInputStream/Constructor.java 2677 java/text/AttributedString/TestAttributedStringCtor.java @@ -2978,6 +3007,7 @@ 2670 java/io/Serializable/partialClassDesc/PartialClassDesc.java 2670 java/io/PrintStream/ClearErrorStream.java 2670 java/beans/XMLDecoder/4676532/Test4676532.java +2669 java/net/SocketOption/TcpKeepAliveTest.java 2668 java/security/cert/CertificateFactory/ReturnStream.java 2668 java/net/URLConnection/contentHandler/UserContentHandler.java 2667 java/security/BasicPermission/MixedBasicPermissions.java @@ -3250,6 +3280,7 @@ 2382 java/util/ResourceBundle/Control/Bug6530694.java 2381 java/io/InputStreamReader/GrowAfterEOF.java 2380 java/nio/channels/Selector/WakeupEmpty.java +2375 java/lang/management/MemoryMXBean/MemoryTest.java#5 2369 java/util/ArrayList/SubListModCount.java 2369 java/text/BreakIterator/Bug4932583.java 2369 java/net/MulticastSocket/TimeToLive.java @@ -3278,6 +3309,7 @@ 2351 java/nio/channels/FileChannel/ScatteringRead.java 2350 java/lang/reflect/Modifier/toStringTest.java 2347 java/net/DatagramSocket/ImplicitBind.java +2346 java/lang/management/OperatingSystemMXBean/GetSystemLoadAverage.java 2345 java/beans/PropertyEditor/TestBooleanClassValue.java 2343 java/lang/Math/Expm1Tests.java 2336 java/security/cert/X509Certificate/GetUnparseableExtensions.java @@ -3602,6 +3634,7 @@ 1982 java/util/ResourceBundle/Bug6572242.java 1981 java/security/cert/URICertStoreParameters/TestBasic.java 1980 java/io/PipedOutputStream/DeadReader.java +1980 java/lang/invoke/DumpMethodHandleInternals.java 1979 java/util/Properties/BlankLines.java 1979 java/net/InetSocketAddress/B4849451.java 1979 java/lang/Class/getModifiers/ResolveFrom.java @@ -3810,6 +3843,7 @@ 1641 java/util/logging/LoggingNIOChange.java 1641 java/net/URL/HashCode.java 1637 javax/xml/jaxp/transform/8004476/XPathExFuncTest.java +1637 java/lang/reflect/Proxy/ProxyModuleMapping.java 1627 java/lang/StringBuffer/SetLength.java 1627 java/io/PushbackInputStream/MarkReset.java 1624 java/lang/annotation/AnnotationTypeMismatchException/ArrayTypeMismatchTest.java diff --git a/test/launcher.test.ts b/test/launcher.test.ts new file mode 100644 index 0000000..564444b --- /dev/null +++ b/test/launcher.test.ts @@ -0,0 +1,121 @@ +// The java launcher's options as the java-web command and the in-VM java take them, +// compared with HotSpot's: -m, --module-path, -version and the module options. + +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { after, before, test } from "node:test"; +import assert from "node:assert/strict"; +import { hotspot } from "../scripts/hotspot.ts"; + +const root = join(import.meta.dirname, ".."); +const node = join(root, "src", "node.ts"); +const dir = mkdtempSync(join(tmpdir(), "launcher-")); +const mods = join(dir, "mods"); +const classes = join(dir, "classes"); + +before(() => { + const m = join(dir, "src", "app"); + mkdirSync(join(m, "app"), { recursive: true }); + const lib = join(dir, "src", "lib"); + mkdirSync(join(lib, "lib"), { recursive: true }); + writeFileSync(join(lib, "module-info.java"), "module lib { exports lib; }\n"); + writeFileSync(join(lib, "lib", "Lib.java"), "package lib;\npublic class Lib { public static String name() { return \"lib\"; } }\n"); + writeFileSync(join(m, "module-info.java"), "module app { requires lib; exports app; }\n"); + writeFileSync(join(m, "app", "Main.java"), `package app; +public class Main { + public static void main(String[] args) throws Exception { + Module m = Main.class.getModule(); + System.out.println(m.getName() + " " + m.isNamed() + " " + lib.Lib.name() + " " + String.join(",", args)); + System.out.println(System.getProperty("jdk.module.main") + " " + ModuleLayer.boot().findModule("app").isPresent()); + if (args.length > 0 && args[0].equals("child")) { + String java = System.getProperty("java.home") + "/bin/java"; + Process p = new ProcessBuilder(java, "-p", System.getProperty("jdk.module.path"), "-m", "app/app.Main", "grandchild").redirectErrorStream(true).start(); + System.out.print(new String(p.getInputStream().readAllBytes())); + System.out.println("exit " + p.waitFor()); + } + } +} +`); + mkdirSync(classes, { recursive: true }); + mkdirSync(join(dir, "boot", "bp"), { recursive: true }); + writeFileSync(join(dir, "boot", "bp", "Hidden.java"), `package bp; +public class Hidden { public static String hello() { return "hello from " + Hidden.class.getClassLoader() + " " + Hidden.class.getModule().isNamed(); } } +`); + writeFileSync(join(dir, "boot", "res.txt"), "resource\n"); + writeFileSync(join(classes, "UseBoot.java"), `public class UseBoot { + public static void main(String[] a) throws Exception { + System.out.println(bp.Hidden.hello()); + System.out.println(Class.forName("bp.Hidden", false, null).getName()); + System.out.println(ClassLoader.getSystemResource("res.txt") != null); + System.out.println(Package.getPackage("bp") != null); + } +} +`); + mkdirSync(join(dir, "patchsrc", "java", "lang"), { recursive: true }); + writeFileSync(join(dir, "patchsrc", "java", "lang", "PatchedX.java"), "package java.lang;\npublic class PatchedX { public static String hi() { return \"patched \" + PatchedX.class.getModule().getName(); } }\n"); + writeFileSync(join(classes, "UsePatch.java"), "public class UsePatch { public static void main(String[] a) throws Exception { System.out.println(Class.forName(\"java.lang.PatchedX\").getMethod(\"hi\").invoke(null)); } }\n"); + writeFileSync(join(classes, "UseLambda.java"), "public class UseLambda { public static void main(String[] a) { Runnable r = () -> System.out.println(\"run\"); r.run(); java.util.function.Supplier s = () -> \"supplied\"; System.out.println(s.get()); } }\n"); + writeFileSync(join(classes, "Plain.java"), "public class Plain { public static void main(String[] a) { System.out.println(\"plain \" + Plain.class.getModule().isNamed()); } }\n"); + execFileSync(hotspot("javac"), ["-d", join(mods, "lib"), join(lib, "module-info.java"), join(lib, "lib", "Lib.java")]); + execFileSync(hotspot("javac"), ["-d", join(mods, "app"), "-p", mods, join(m, "module-info.java"), join(m, "app", "Main.java")]); + execFileSync(hotspot("javac"), ["-d", join(dir, "boot"), join(dir, "boot", "bp", "Hidden.java")]); + execFileSync(hotspot("javac"), ["-d", join(dir, "patch"), "--patch-module", `java.base=${join(dir, "patchsrc")}`, join(dir, "patchsrc", "java", "lang", "PatchedX.java")]); + execFileSync(hotspot("javac"), ["-d", classes, "-cp", join(dir, "boot"), join(classes, "Plain.java"), join(classes, "UseBoot.java"), join(classes, "UsePatch.java"), join(classes, "UseLambda.java")]); + execFileSync(hotspot("jar"), ["cf", join(dir, "boot.jar"), "-C", join(dir, "boot"), "bp", "-C", join(dir, "boot"), "res.txt"]); +}); +after(() => rmSync(dir, { recursive: true, force: true })); + +const run = (java: string, args: string[]) => { + const got = java === "hotspot" ? spawnSync(hotspot("java"), args, { encoding: "utf8" }) + : spawnSync(process.execPath, [node, ...args], { encoding: "utf8" }); + return { stdout: got.stdout, stderr: got.stderr, code: got.status }; +}; + +for (const [name, args] of [ + ["-m with a class", () => ["-p", mods, "-m", "app/app.Main", "a", "b"]], + ["--module=", () => ["--module-path", mods, "--module=app/app.Main", "x"]], + ["-m, a nested -m child", () => ["-p", mods, "-m", "app/app.Main", "child"]], + ["--add-modules on the class path", () => ["--module-path", mods, "--add-modules", "app", "-cp", classes, "Plain"]], + ["--limit-modules", () => ["--limit-modules", "java.base", "-cp", classes, "Plain"]], + ["-Xbootclasspath/a with a directory", () => [`-Xbootclasspath/a:${join(dir, "boot")}`, "-cp", classes, "UseBoot"]], + ["-Xbootclasspath/a with a jar", () => [`-Xbootclasspath/a:${join(dir, "boot.jar")}`, "-cp", classes, "UseBoot"]], + ["-Xbootclasspath/a twice", () => [`-Xbootclasspath/a:${join(dir, "boot")}`, `-Xbootclasspath/a:${join(dir, "patch")}`, "-cp", classes, "UseBoot"]], + ["-Xbootclasspath/a twice, the one used last", () => [`-Xbootclasspath/a:${join(dir, "patch")}`, `-Xbootclasspath/a:${join(dir, "boot")}`, "-cp", classes, "UseBoot"]], + ["--patch-module of a boot loader module", () => ["--patch-module", `java.base=${join(dir, "patch")}`, "-cp", classes, "UsePatch"]], + ["no such module", () => ["-p", mods, "-m", "missing/x.Main"]], + ["-m without a module", () => ["-m"]], + ["--module= without a module", () => ["--module="]], +] as [string, () => string[]][]) { + test(name, () => { + const a = args(); + const ref = run("hotspot", a); + assert.deepEqual(run("java-web", a), ref); + }); +} + +test("-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles writes the lambda classes", () => { + const listing = (java: string) => { + const cwd = mkdtempSync(join(dir, "dump-")); + const got = java === "hotspot" ? spawnSync(hotspot("java"), ["-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles", "-cp", classes, "UseLambda"], { cwd, encoding: "utf8" }) + : spawnSync(process.execPath, [node, "-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles", "-cp", classes, "UseLambda"], { cwd, encoding: "utf8" }); + const files = readdirSync(join(cwd, "DUMP_LAMBDA_PROXY_CLASS_FILES"), { recursive: true, encoding: "utf8" }) + .filter((f) => f.startsWith("UseLambda")).map((f) => f.replace(/0x[0-9a-f]+/, "0x")).sort(); + return { stdout: got.stdout, code: got.status, files }; + }; + const ref = listing("hotspot"); + assert.ok(ref.files.length >= 2); + assert.deepEqual(listing("java-web"), ref); +}); + +test("-version prints three lines on stderr", () => { + const ref = run("hotspot", ["-version"]); + const got = run("java-web", ["-version"]); + assert.equal(got.code, 0); + assert.equal(got.stdout, ""); + const [first, second] = got.stderr.split("\n"); + assert.equal(first, ref.stderr.split("\n")[0]); + assert.match(second, /^OpenJDK Runtime Environment /); + assert.equal(got.stderr.split("\n").length, ref.stderr.split("\n").length); +}); diff --git a/test/timezone.test.ts b/test/timezone.test.ts new file mode 100644 index 0000000..6d21709 --- /dev/null +++ b/test/timezone.test.ts @@ -0,0 +1,62 @@ +// The default time zone from a TZ value that is a POSIX rule, not a zone ID, +// compared with HotSpot's. + +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { after, before, test } from "node:test"; +import assert from "node:assert/strict"; +import { hotspot } from "../scripts/hotspot.ts"; +import { posixOffsetMinutes } from "../src/posixtz.ts"; + +const dir = mkdtempSync(join(tmpdir(), "timezone-")); +before(() => { + writeFileSync(join(dir, "Zone.java"), `import java.util.*; +public class Zone { + public static void main(String[] a) { + TimeZone tz = TimeZone.getDefault(); + long now = System.currentTimeMillis(); + System.out.println(tz.getID() + " " + tz.getOffset(now) + " " + new Date(now).toString().contains(tz.getID())); + } +} +`); + execFileSync(hotspot("javac"), ["-d", dir, join(dir, "Zone.java")]); +}); +after(() => rmSync(dir, { recursive: true, force: true })); + +// both hemispheres, a zone with no daylight time, one with the default rules +for (const tz of ["MEZ-1MESZ,M3.5.0,M10.5.0/3", "MEZ-1MESZ,M10.5.0,M3.5.0/3", "AAA-3", "AAA-1BBB,M1.1.0,M12.5.0", "AAA-1BBB,M12.5.0,M1.1.0", "AAA5BBB", "Europe/Berlin"]) { + test(`TZ=${tz}`, () => { + const run = (cmd: string, args: string[]) => { + const got = spawnSync(cmd, args, { encoding: "utf8", env: { ...process.env, TZ: tz } }); + return { stdout: got.stdout, code: got.status }; + }; + assert.deepEqual( + run(process.execPath, [join(import.meta.dirname, "..", "src", "node.ts"), "-cp", dir, "Zone"]), + run(hotspot("java"), ["-cp", dir, "Zone"]), + ); + }); +} + +test("posixOffsetMinutes follows the rule dates", () => { + const tz = "MEZ-1MESZ,M3.5.0,M10.5.0/3"; + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 0, 15)), 60); + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 6, 15)), 120); + // 2026-03-29 01:00 UTC is the switch + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 2, 29, 0, 59)), 60); + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 2, 29, 1, 0)), 120); + // 2026-10-25 01:00 UTC is the switch back (03:00 local daylight time) + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 9, 25, 0, 59)), 120); + assert.equal(posixOffsetMinutes(tz, Date.UTC(2026, 9, 25, 1, 0)), 60); + assert.equal(posixOffsetMinutes("MEZ-1MESZ,M10.5.0,M3.5.0/3", Date.UTC(2026, 0, 15)), 120); + assert.equal(posixOffsetMinutes("EST5EDT", Date.UTC(2026, 6, 15)), -240); + assert.equal(posixOffsetMinutes("nonsense", 0), null); + assert.equal(posixOffsetMinutes("", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,M3.2.0", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,M13.2.0,M11.1.0", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,M3.6.0,M11.1.0", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,M3.2.0/x,M11.1.0", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,M3.2.0/2/3,M11.1.0", 0), null); + assert.equal(posixOffsetMinutes("AAA5BBB,J0,J300", 0), null); +}); diff --git a/types/node.d.ts b/types/node.d.ts index 18e2ebf..fa6e26a 100644 --- a/types/node.d.ts +++ b/types/node.d.ts @@ -4,6 +4,8 @@ export interface NodeRunOptions { classpath: string[]; mainClass?: string; + // "module" or "module/class" from the module path in props (jdk.module.path) + mainModule?: string; jar?: string; args?: string[]; // system properties