diff --git a/src/image.ts b/src/image.ts index 9bc3fed..5f9cdec 100644 --- a/src/image.ts +++ b/src/image.ts @@ -8,17 +8,22 @@ import { listIndex, packedRange, parsePack } from "./browser/http.ts"; import type { Pack } from "./browser/http.ts"; import type { Backend, Stat } from "./fs.ts"; -// The JDK image is read for every boot class, by every VM in the process, so -// each image file stays open once opened. +// The JDK image is read for every boot class, so each image file stays open +// once opened, shared by the VMs that read it. A VM keeps the descriptor it +// started with: an upgrade can replace the file while VMs that read its index +// are still running, and new VMs then open the new file. const imageFds = new Map(); export function imageFd(file: string): number { - let fd = imageFds.get(file); - if (fd === undefined) imageFds.set(file, (fd = fs.openSync(file, "r"))); + const key = `${file}\0${fs.statSync(file).ino}`; + let fd = imageFds.get(key); + if (fd === undefined) imageFds.set(key, (fd = fs.openSync(file, "r"))); return fd; } export class ImageBackend implements Backend { readonly writable = false; + // this VM's descriptor of each image file + private readonly fds = new Map(); private readonly dir: string; private readonly pack: Pack; @@ -55,7 +60,8 @@ export class ImageBackend implements Backend { const out = new Uint8Array(len); let n = 0; try { - const fd = imageFd(path.join(this.dir, "jdk", at.image)); + let fd = this.fds.get(at.image); + if (fd === undefined) this.fds.set(at.image, (fd = imageFd(path.join(this.dir, "jdk", at.image)))); while (n < len) { const got = fs.readSync(fd, out, n, len - n, at.at + off + n); if (got === 0) break; diff --git a/src/node.ts b/src/node.ts index c523f8e..3c5259e 100644 --- a/src/node.ts +++ b/src/node.ts @@ -71,10 +71,12 @@ export class NodeBackend implements Backend { } // Other open files keep their buffered contents even if the host removes them. canReadRange(p: string) { return !this.writable && p === "/lib/modules"; } + // this VM's descriptor of the JDK image + private imageFd: number | undefined; readRange(p: string, off: number, len: number): Uint8Array | null { const image = p === "/lib/modules" && !this.writable; try { - const fd = image ? imageFd(this.host(p)) : fs.openSync(this.host(p), "r"); + const fd = image ? (this.imageFd ??= imageFd(this.host(p))) : fs.openSync(this.host(p), "r"); try { if (!image && !fs.fstatSync(fd).isFile()) return null; const out = new Uint8Array(len); diff --git a/test/image.test.ts b/test/image.test.ts index 9a5751c..0a5b1c6 100644 --- a/test/image.test.ts +++ b/test/image.test.ts @@ -3,7 +3,7 @@ // dist/ is built. import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; @@ -65,6 +65,25 @@ test("ImageBackend reads the index, packed files and module images", () => { } }); +test("a VM keeps reading the image it started with when an upgrade replaces it", () => { + const dir = mkdtempSync(join(tmpdir(), "jw-image-")); + try { + mkdirSync(join(dir, "jdk")); + writeFileSync(join(dir, "jdk", "image.jar"), "OLD!"); + writeFileSync(join(dir, "boot.pack"), pack([["/index.txt", "d /lib\nf /lib/x 4 image:0"]])); + const text = (d: Uint8Array | null) => d && new TextDecoder().decode(d); + const before = new ImageBackend(dir); + assert.equal(text(before.read("/lib/x")), "OLD!"); + // as a package manager replaces a file: a new one renamed over it + writeFileSync(join(dir, "jdk", "image.new"), "NEW!"); + renameSync(join(dir, "jdk", "image.new"), join(dir, "jdk", "image.jar")); + assert.equal(text(new ImageBackend(dir).read("/lib/x")), "NEW!"); + assert.equal(text(before.readRange("/lib/x", 0, 4)), "OLD!"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test("compact index keeps paths, sizes, offsets, and directory order", () => { const buf = pack([["/index.prefix", "0 d /lib\n0 f /lib/one 3 image:0\n7 two 4 image:3 javac\n0 d /modules"]]); const image = parsePack(buf.buffer as ArrayBuffer);