From bbd01923e96ccd3f727549b2a01c61188dae247c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:15:07 +0800 Subject: [PATCH] fix(artifacts): own DMG readers before attachment Make inventory reader construction synchronous and DMG attachment lazy, so the inventory registers its reader before native operations start. Share initialization, reject work after close, wait for an in-flight attach before cleanup, and preserve retryable close state. Cover failed-attach inventory cleanup, concurrent initialization, close-before-use, and close-during-attach alongside existing ownership, cancellation, and integrity cases. Session-level retention after inventory cleanup failure remains a separate concern. --- src/artifacts/NativeDmgArtifactReader.ts | 69 ++-- src/artifacts/inventory/reader.ts | 7 +- src/artifacts/inventory/scanCanonical.ts | 3 +- .../artifactInventorySnapshot.test.ts | 2 +- .../filesystem/asarInventoryFreshness.test.ts | 4 +- .../native/nativeDmgArtifactReader.test.ts | 348 ++++++++++++++---- 6 files changed, 325 insertions(+), 108 deletions(-) diff --git a/src/artifacts/NativeDmgArtifactReader.ts b/src/artifacts/NativeDmgArtifactReader.ts index 9ff38905b..d62bdcd72 100644 --- a/src/artifacts/NativeDmgArtifactReader.ts +++ b/src/artifacts/NativeDmgArtifactReader.ts @@ -111,7 +111,9 @@ const createSystemHost = ( /** Read-only macOS DMG adapter that owns attachment and reverse-order detach. */ export class NativeDmgArtifactReader implements ArtifactReader { readonly format = "file" as const; + private readonly host: NativeDmgHost; readonly #provenance: ArtifactCommand[] = []; + #initialization: Promise | undefined; #directory: DirectoryArtifactReader | undefined; #devices: string[] = []; #inventoryConfirmedDevices = new Set(); @@ -121,33 +123,24 @@ export class NativeDmgArtifactReader implements ArtifactReader { #attachOutputReturned = false; #attachMayHaveMounted = false; #ownershipUncertainty: string | undefined; + #closed = false; + #closePromise: Promise | undefined; - private constructor( + constructor( private readonly path: string, - private readonly host: NativeDmgHost, - ) {} - - /** Verify and attach one image beneath an exclusively owned temporary root. */ - static async create( - path: string, environment: Readonly, - signal?: AbortSignal, host?: NativeDmgHost, - ): Promise { + ) { if (process.platform !== "darwin" && host === undefined) throw new ArtifactReaderFailure( "unavailable", "Native DMG traversal is available only on macOS", ); - const reader = new NativeDmgArtifactReader( - path, - host ?? createSystemHost(environment), - ); - await reader.attach(signal); - return reader; + this.host = host ?? createSystemHost(environment); } async *entries(signal?: AbortSignal): AsyncIterable { + await this.#ensureAttached(signal); if (this.#directory === undefined) throw new ArtifactReaderFailure("unavailable", "DMG is not attached"); const prefix = basename(this.path); @@ -155,11 +148,10 @@ export class NativeDmgArtifactReader implements ArtifactReader { yield { ...entry, path: `${prefix}/${entry.path}` }; } - open(entry: ArtifactEntry, signal?: AbortSignal): Promise { + async open(entry: ArtifactEntry, signal?: AbortSignal): Promise { + await this.#ensureAttached(signal); if (this.#directory === undefined) - return Promise.reject( - new ArtifactReaderFailure("unavailable", "DMG is not attached"), - ); + throw new ArtifactReaderFailure("unavailable", "DMG is not attached"); return this.#directory.open(entry, signal); } @@ -167,7 +159,19 @@ export class NativeDmgArtifactReader implements ArtifactReader { return structuredClone(this.#provenance); } - async close(): Promise { + close(): Promise { + this.#closed = true; + this.#closePromise ??= this.#closeOwnedResources().catch( + (cause: unknown) => { + this.#closePromise = undefined; + throw cause; + }, + ); + return this.#closePromise; + } + + async #closeOwnedResources(): Promise { + await this.#initialization?.catch(() => undefined); let detachFailure = await this.#reconcileAttachState(); let remainingDevices: string[] = []; for (const device of [...this.#devices].reverse()) { @@ -281,7 +285,16 @@ export class NativeDmgArtifactReader implements ArtifactReader { } } - async attach(signal?: AbortSignal): Promise { + async #ensureAttached(signal?: AbortSignal): Promise { + if (this.#closed) + throw new ArtifactReaderFailure("unavailable", "DMG reader is closed"); + this.#initialization ??= this.#attach(signal); + await this.#initialization; + if (this.#closed) + throw new ArtifactReaderFailure("unavailable", "DMG reader is closed"); + } + + async #attach(signal?: AbortSignal): Promise { await runChecked(this.host, ["verify", this.path], signal); this.#provenance.push(command(["verify", this.path], ["read"])); const baseline = await this.#readInfo(); @@ -361,20 +374,6 @@ export class NativeDmgArtifactReader implements ArtifactReader { this.#attachMayHaveMounted = false; this.#ownershipUncertainty = undefined; } - let cleanupFailure: unknown; - try { - await this.close(); - } catch (cleanupCause: unknown) { - cleanupFailure = cleanupCause; - } - if (cleanupFailure !== undefined) - throw ArtifactReaderFailure.withCleanup( - cause, - ArtifactReaderFailure.cleanupObservation( - cleanupFailure, - `DMG mount root ${this.#mountRoot ?? this.path}`, - ), - ); throw cause; } } diff --git a/src/artifacts/inventory/reader.ts b/src/artifacts/inventory/reader.ts index 33e5a195a..471ca1ca7 100644 --- a/src/artifacts/inventory/reader.ts +++ b/src/artifacts/inventory/reader.ts @@ -8,13 +8,12 @@ import type { ArtifactOccurrence } from "../../domain/artifactGraph.js"; import { ZIP_NON_ENTRY_TAIL_LIMITATION } from "../../domain/zipPackageFormat.js"; import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js"; -export const createReader = async ( +export const createReader = ( path: string, format: ArtifactOccurrence["artifact_format"], environment: Readonly, - signal?: AbortSignal, rootSource?: StableRegularFileDescriptor, -): Promise => { +): ArtifactReader | undefined => { switch (format) { case "directory": return new DirectoryArtifactReader(path); @@ -32,7 +31,7 @@ export const createReader = async ( : undefined; case "dmg": if (process.platform !== "darwin") return undefined; - return NativeDmgArtifactReader.create(path, environment, signal); + return new NativeDmgArtifactReader(path, environment); default: return undefined; } diff --git a/src/artifacts/inventory/scanCanonical.ts b/src/artifacts/inventory/scanCanonical.ts index bec77ca1f..929760002 100644 --- a/src/artifacts/inventory/scanCanonical.ts +++ b/src/artifacts/inventory/scanCanonical.ts @@ -68,11 +68,10 @@ export const scanCanonicalArtifactInventory = async ( let reader: ArtifactReader | undefined; let readerCreationFailure: { readonly cause: unknown } | undefined; try { - reader = await readerFactory( + reader = readerFactory( path, rootFormat, options.environment ?? {}, - options.signal, rootSource, ); } catch (cause: unknown) { diff --git a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts index e55f01971..337bb7c70 100644 --- a/tests/boundary/filesystem/artifactInventorySnapshot.test.ts +++ b/tests/boundary/filesystem/artifactInventorySnapshot.test.ts @@ -53,7 +53,7 @@ describe("artifact inventory snapshot", () => { const failure = await scanCanonicalArtifactInventory( root, {}, - async () => reader, + () => reader, ).catch((cause: unknown) => cause); expect(failure).toBeInstanceOf(ArtifactReaderFailure); if (!(failure instanceof ArtifactReaderFailure)) diff --git a/tests/boundary/filesystem/asarInventoryFreshness.test.ts b/tests/boundary/filesystem/asarInventoryFreshness.test.ts index da9db6731..3b7cedcfa 100644 --- a/tests/boundary/filesystem/asarInventoryFreshness.test.ts +++ b/tests/boundary/filesystem/asarInventoryFreshness.test.ts @@ -176,7 +176,7 @@ describe("ASAR captured-byte identity", () => { const originalSize = (await lstat(fixture.archive)).size; const directory = new DirectoryArtifactReader(fixture.bundle); await expect( - scanCanonicalArtifactInventory(fixture.bundle, {}, async () => ({ + scanCanonicalArtifactInventory(fixture.bundle, {}, () => ({ format: directory.format, entries: (signal) => directory.entries(signal), provenance: () => directory.provenance(), @@ -254,7 +254,7 @@ describe("ASAR captured-byte identity", () => { const failure: unknown = await scanCanonicalArtifactInventory( fixture.archive, {}, - async () => reader, + () => reader, ).catch((cause: unknown) => cause); expect(failure).toMatchObject({ cleanup: { resources: [attempts[0]] }, diff --git a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts index 276db13e1..11a63a976 100644 --- a/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts +++ b/tests/boundary/providers/native/nativeDmgArtifactReader.test.ts @@ -18,6 +18,43 @@ import { NativeDmgArtifactReader, type NativeDmgHost, } from "../../../../src/artifacts/NativeDmgArtifactReader.js"; +import { scanCanonicalArtifactInventory } from "../../../../src/artifacts/inventory/scanCanonical.js"; + +const createAttachedReader = async ( + path: string, + environment: Readonly, + signal?: AbortSignal, + host?: NativeDmgHost, +): Promise => { + const reader = new NativeDmgArtifactReader(path, environment, host); + try { + for await (const _entry of reader.entries(signal)) { + // Force lazy initialization while keeping the existing lifecycle tests concise. + } + return reader; + } catch (cause: unknown) { + try { + await reader.close(); + } catch (cleanupCause: unknown) { + throw ArtifactReaderFailure.withCleanup( + cause, + ArtifactReaderFailure.cleanupObservation( + cleanupCause, + `DMG mount root ${path}`, + ), + ); + } + throw cause; + } +}; + +const entryPaths = async ( + reader: NativeDmgArtifactReader, +): Promise => { + const paths: string[] = []; + for await (const entry of reader.entries()) paths.push(entry.path); + return paths; +}; const infoPlist = ( imagePath: string, @@ -119,6 +156,222 @@ const createOmittedInventoryHost = () => { return { host, state }; }; +describe("DMG inventory reader ownership", () => { + it("lets inventory clean up a lazy reader after attach fails", async () => { + const parent = await createTestTempDirectory("rea-dmg-inventory-owner-"); + const path = join(parent, "image.dmg"); + const image = Buffer.alloc(512); + image.write("koly", 0, "ascii"); + await writeFile(path, image); + + const calls: string[][] = []; + const mount = createMountFixture(); + let attached = false; + let mountPoint: string | undefined; + let infoAvailable = false; + const host: NativeDmgHost = { + async run(arguments_) { + calls.push([...arguments_]); + if (arguments_[0] === "verify") return { stdout: "", exitCode: 0 }; + if (arguments_[0] === "info") { + if (!attached) return { stdout: emptyInfoPlist, exitCode: 0 }; + if (!infoAvailable) + return { + stdout: "", + stderr: "temporary hdiutil info failure", + exitCode: 1, + }; + return { + stdout: infoPlist(path, [ + { + "dev-entry": "/dev/disk-inventory-owner", + ...(mountPoint === undefined + ? {} + : { "mount-point": mountPoint }), + }, + ]), + exitCode: 0, + }; + } + if (arguments_[0] === "attach") { + ({ mountPoint } = await mount.createVolume(arguments_, "owned")); + attached = true; + return { stdout: "malformed attach plist", exitCode: 0 }; + } + if (arguments_[0] === "detach") { + attached = false; + return { stdout: "", exitCode: 0 }; + } + throw new Error(`unexpected hdiutil command: ${arguments_[0] ?? ""}`); + }, + delay: () => Promise.resolve(), + }; + + let reader: NativeDmgArtifactReader | undefined; + let closeCalls = 0; + const failure = await scanCanonicalArtifactInventory( + path, + {}, + (inputPath, _format, environment) => { + reader = new NativeDmgArtifactReader(inputPath, environment, host); + const closeReader = reader.close.bind(reader); + reader.close = () => { + closeCalls += 1; + return closeReader(); + }; + return reader; + }, + ).catch((cause: unknown) => cause); + + expect(failure).toMatchObject({ + reason: "format", + cleanup: { + resources: expect.arrayContaining([ + expect.stringMatching(/^DMG attachment ownership unknown/u), + expect.stringMatching(/^DMG mount root /u), + ]), + }, + }); + if (reader === undefined) + throw new Error("Inventory did not construct its DMG reader"); + expect(closeCalls).toBe(1); + const mountRoot = mount.root(); + if (mountRoot === undefined) throw new Error("missing owned mount root"); + expect(await realpath(mountRoot)).toBe(mountRoot); + + infoAvailable = true; + await reader.close(); + expect(attached).toBe(false); + await expect(realpath(mountRoot)).rejects.toMatchObject({ code: "ENOENT" }); + expect(calls.map(([operation]) => operation)).toEqual([ + "verify", + "info", + "attach", + "info", + "info", + "detach", + ]); + }); +}); + +describe("native DMG lazy initialization", () => { + it("does not attach before entries and stays closed when closed first", async () => { + const calls: string[][] = []; + const reader = new NativeDmgArtifactReader("/tmp/image.dmg", process.env, { + run: (arguments_) => { + calls.push([...arguments_]); + return Promise.resolve({ stdout: "", exitCode: 0 }); + }, + }); + + await reader.close(); + await expect(entryPaths(reader)).rejects.toMatchObject({ + reason: "unavailable", + }); + expect(calls).toEqual([]); + }); + + it("shares one lazy attach across concurrent entries calls", async () => { + const { host, state } = createOmittedInventoryHost(); + const reader = new NativeDmgArtifactReader( + "/tmp/image.dmg", + process.env, + host, + ); + try { + const [first, second] = await Promise.all([ + entryPaths(reader), + entryPaths(reader), + ]); + expect(first).toEqual([ + "image.dmg/Fixture", + "image.dmg/Fixture/hello.txt", + ]); + expect(second).toEqual(first); + expect( + state.calls.filter(([operation]) => operation === "attach"), + ).toHaveLength(1); + } finally { + state.detachAttempts = 4; + await reader.close(); + } + }); + + it("waits for a shared attach before close and rejects its pending entries", async () => { + const calls: string[][] = []; + const mount = createMountFixture(); + let markAttachStarted = (): void => {}; + const attachStarted = new Promise((resolve) => { + markAttachStarted = resolve; + }); + let finishAttach = (_result: { + readonly stdout: string; + readonly exitCode: number; + }): void => {}; + let attached = false; + let mountPoint: string | undefined; + const host: NativeDmgHost = { + async run(arguments_) { + calls.push([...arguments_]); + if (arguments_[0] === "info") + return { + stdout: + !attached || mountPoint === undefined + ? emptyInfoPlist + : infoPlist("/tmp/image.dmg", [ + { "dev-entry": "/dev/disk70", "mount-point": mountPoint }, + ]), + exitCode: 0, + }; + if (arguments_[0] === "attach") { + const mounted = await mount.createVolume(arguments_, "owned"); + mountPoint = mounted.mountPoint; + markAttachStarted(); + return new Promise((resolve) => { + finishAttach = resolve; + }); + } + if (arguments_[0] === "detach") attached = false; + return { stdout: "", exitCode: 0 }; + }, + }; + const reader = new NativeDmgArtifactReader( + "/tmp/image.dmg", + process.env, + host, + ); + + const entries = entryPaths(reader); + await attachStarted; + let closeFinished = false; + const closing = reader.close().then(() => { + closeFinished = true; + }); + await Promise.resolve(); + expect(closeFinished).toBe(false); + + attached = true; + finishAttach({ + stdout: attachPlist([ + { "dev-entry": "/dev/disk70", "mount-point": mountPoint ?? "" }, + ]), + exitCode: 0, + }); + + await expect(entries).rejects.toMatchObject({ reason: "unavailable" }); + await closing; + expect(calls.filter(([operation]) => operation === "attach")).toHaveLength( + 1, + ); + expect(calls.filter(([operation]) => operation === "detach")).toEqual([ + ["detach", "/dev/disk70"], + ]); + expect( + await realpath(mount.root() ?? "").catch(() => undefined), + ).toBeUndefined(); + }); +}); + describe("native DMG artifact reader", () => { it("uses plist attachment metadata and detaches returned devices", async () => { const calls: string[][] = []; @@ -158,7 +411,7 @@ describe("native DMG artifact reader", () => { }; }, }; - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -179,7 +432,7 @@ describe("native DMG artifact reader", () => { it("rejects non-zero verification results", async () => { await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run: () => Promise.resolve({ stdout: "verify output", @@ -221,12 +474,7 @@ describe("native DMG artifact reader", () => { }, }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toMatchObject({ reason: "format", cleanup: { @@ -246,7 +494,7 @@ describe("native DMG artifact reader", () => { describe("DMG attach recovery", () => { it("keeps attach-output ownership when the first inventory omits the mount", async () => { const { host, state } = createOmittedInventoryHost(); - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -327,7 +575,7 @@ describe("DMG rooted ownership isolation", () => { }, }; - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -375,12 +623,7 @@ describe("DMG failed attach output recovery", () => { }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toThrow(/hdiutil attach failed/u); expect(calls).toContainEqual(["detach", "/dev/disk25"]); expect( @@ -427,12 +670,7 @@ describe("DMG attach fallback recovery", () => { }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toThrow(); expect(calls.map(([operation]) => operation)).toEqual([ "verify", @@ -472,12 +710,7 @@ describe("DMG attach fallback recovery", () => { }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toMatchObject({ reason: "unavailable", cleanup: { @@ -551,7 +784,7 @@ describe("DMG attach cancellation and preexisting mounts", () => { }, }; - const creating = NativeDmgArtifactReader.create( + const creating = createAttachedReader( "/tmp/image.dmg", process.env, controller.signal, @@ -600,12 +833,7 @@ describe("DMG attach cancellation and preexisting mounts", () => { }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toMatchObject({ reason: "path" }); expect(calls.filter(([operation]) => operation === "detach")).toEqual([]); expect(calls.map(([operation]) => operation)).toEqual([ @@ -647,12 +875,7 @@ describe("native DMG command failure diagnostics", () => { }; await expect( - NativeDmgArtifactReader.create( - "/tmp/image.dmg", - process.env, - undefined, - host, - ), + createAttachedReader("/tmp/image.dmg", process.env, undefined, host), ).rejects.toMatchObject({ reason: "unavailable", message: expect.stringContaining('"code":"ENOENT"'), @@ -672,7 +895,7 @@ describe("native DMG command failure diagnostics", () => { describe("native DMG command diagnostics", () => { it("keeps unknown attach failures unavailable with command evidence", async () => { await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run(arguments_) { if (arguments_[0] === "verify") return Promise.resolve({ stdout: "", exitCode: 0 }); @@ -697,7 +920,7 @@ describe("native DMG command diagnostics", () => { }); it("keeps unrecognized numeric verify failures unknown and maps the observed I/O diagnostic", async () => { - const unknown = await NativeDmgArtifactReader.create( + const unknown = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -719,7 +942,7 @@ describe("native DMG command diagnostics", () => { expect(unknown.message).toContain("an unrecognized system failure"); await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run: () => Promise.resolve({ stdout: "", @@ -733,7 +956,7 @@ describe("native DMG command diagnostics", () => { }); await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run: () => Promise.resolve({ stdout: "checksum output", @@ -749,7 +972,7 @@ describe("native DMG command diagnostics", () => { it("distinguishes child launch and host permission failures", async () => { await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run: () => Promise.reject( Object.assign(new Error("could not launch hdiutil"), { @@ -764,7 +987,7 @@ describe("native DMG command diagnostics", () => { }); await expect( - NativeDmgArtifactReader.create("/tmp/image.dmg", process.env, undefined, { + createAttachedReader("/tmp/image.dmg", process.env, undefined, { run: () => Promise.reject( Object.assign(new Error("permission denied"), { @@ -787,10 +1010,9 @@ describe("native DMG real verification diagnostics", () => { const root = await createTestTempDirectory("rea-invalid-dmg-"); const path = join(root, "invalid.dmg"); await writeFile(path, "This is not a disk image.\n"); - const failure = await NativeDmgArtifactReader.create( - path, - process.env, - ).catch((cause: unknown) => cause); + const failure = await createAttachedReader(path, process.env).catch( + (cause: unknown) => cause, + ); expect(failure).toBeInstanceOf(ArtifactReaderFailure); if (!(failure instanceof ArtifactReaderFailure)) return; expect(failure.reason).toBe("format"); @@ -815,10 +1037,9 @@ describe("native DMG real verification diagnostics", () => { [missingPath, "No such file or directory"], [unreadablePath, "Permission denied"], ] as const) { - const failure = await NativeDmgArtifactReader.create( - path, - process.env, - ).catch((cause: unknown) => cause); + const failure = await createAttachedReader(path, process.env).catch( + (cause: unknown) => cause, + ); expect(failure).toBeInstanceOf(ArtifactReaderFailure); if (!(failure instanceof ArtifactReaderFailure)) continue; expect(failure.reason).toBe("io"); @@ -888,10 +1109,9 @@ describe("native DMG real verification diagnostics", () => { image[corruptionOffset] = originalByte ^ 0x01; await writeFile(imagePath, image); - const failure = await NativeDmgArtifactReader.create( - imagePath, - process.env, - ).catch((cause: unknown) => cause); + const failure = await createAttachedReader(imagePath, process.env).catch( + (cause: unknown) => cause, + ); expect(failure).toBeInstanceOf(ArtifactReaderFailure); if (!(failure instanceof ArtifactReaderFailure)) return; expect(failure.reason).toBe("integrity"); @@ -953,7 +1173,7 @@ it("owns the canonical mount root and detaches each observed whole image once", }; }, }; - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -1103,7 +1323,7 @@ const closeApfsFixture = async ( readonly cleanup: readonly string[][]; }> => { const { calls, host } = apfsHost(...listing); - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -1135,7 +1355,7 @@ describe("APFS disk image detach", () => { it("still fails when the device that rejected detach remains attached", async () => { const { host } = apfsHost(["/dev/disk4", "/dev/disk4s1"]); - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -1156,7 +1376,7 @@ describe("APFS disk image detach", () => { it("retains a failed attachment and its mount root until a later close detaches it", async () => { let busy = true; const { host, state } = singleDeviceHost(() => busy); - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined, @@ -1201,7 +1421,7 @@ describe("DMG detach recheck", () => { it("detaches a device that was busy on its first attempt within one close", async () => { const { host, state } = singleDeviceHost((attempt) => attempt === 1); - const reader = await NativeDmgArtifactReader.create( + const reader = await createAttachedReader( "/tmp/image.dmg", process.env, undefined,