diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc index c539166d34..f8b88aabc1 100644 --- a/native/windows/src/filesystem.cc +++ b/native/windows/src/filesystem.cc @@ -642,9 +642,10 @@ static void completeSnapshot(napi_env env, napi_status status, void* pointer) { disposeSnapshot(*work->root, work->destination, work->path); napi_reject_deferred(env, work->deferred, failureValue(env, failure)); } catch (const Failure& cleanupFailure) { - const Failure combined( - failure.constraint + "; incomplete snapshot cleanup failed: " + cleanupFailure.constraint, - work->path, cleanupFailure.win32); + Failure combined = failure; + combined.cleanupConstraint = cleanupFailure.constraint; + combined.cleanupWin32 = cleanupFailure.win32; + combined.cleanupPath = cleanupFailure.path; napi_reject_deferred(env, work->deferred, failureValue(env, combined)); } }; diff --git a/src/windows/WindowsPrivateRuntime.test.ts b/src/windows/WindowsPrivateRuntime.test.ts index 08cfda6246..e32cf2c068 100644 --- a/src/windows/WindowsPrivateRuntime.test.ts +++ b/src/windows/WindowsPrivateRuntime.test.ts @@ -13,6 +13,7 @@ vi.mock("./WindowsNativeLoader.js", async (importOriginal) => { import { WindowsPrivateRuntime, + WindowsSnapshotCancelledError, windowsPrivateRuntime, } from "./WindowsPrivateRuntime.js"; @@ -149,3 +150,29 @@ describe("Windows private runtime cleanup retry", () => { await runtime.close(); }); }); + +it.each([995, 5])( + "classifies native snapshot code %i independently of a late abort", + async (code) => { + const controller = new AbortController(); + const failure = nativeFailure(code); + nativeCall.mockImplementation((operation: string) => { + if (operation === "runtime_create") return observation; + if (operation === "runtime_snapshot") return Promise.reject(failure); + return null; + }); + const runtime = WindowsPrivateRuntime.create("C:\\", "rea-"); + const pending = runtime.snapshot( + "C:\\target.bin", + "target.bin", + controller.signal, + ); + controller.abort(null); + const result: unknown = await pending.catch((cause: unknown) => cause); + if (code === 995) { + expect(result).toBeInstanceOf(WindowsSnapshotCancelledError); + expect(result).toMatchObject({ reason: null, cause: failure }); + } else expect(result).toBe(failure); + await runtime.close(); + }, +); diff --git a/src/windows/WindowsPrivateRuntime.ts b/src/windows/WindowsPrivateRuntime.ts index e1cd227ee7..ea28a5398f 100644 --- a/src/windows/WindowsPrivateRuntime.ts +++ b/src/windows/WindowsPrivateRuntime.ts @@ -36,6 +36,22 @@ const transientRuntimeCleanupSchema = z.object({ code: z.literal("ERR_REA_WINDOWS_NATIVE"), win32Code: z.union([z.literal(32), z.literal(33)]), }); +const snapshotCancellationSchema = z.object({ + code: z.literal("ERR_REA_WINDOWS_NATIVE"), + win32Code: z.literal(995), +}); + +/** A native snapshot acknowledged cancellation, with the caller reason retained. */ +export class WindowsSnapshotCancelledError extends Error { + constructor( + readonly reason: unknown, + cause: unknown, + ) { + super("Windows snapshot cancelled", { cause }); + this.name = "WindowsSnapshotCancelledError"; + } +} + const RUNTIME_CLEANUP_ATTEMPTS = 10; const RUNTIME_CLEANUP_RETRY_DELAY_MS = 100; @@ -114,6 +130,13 @@ export class WindowsPrivateRuntime { if (signal?.aborted === true) cancel(); try { return snapshotSchema.parse(await pending); + } catch (cause: unknown) { + if ( + signal?.aborted === true && + snapshotCancellationSchema.safeParse(cause).success + ) + throw new WindowsSnapshotCancelledError(signal.reason, cause); + throw cause; } finally { signal?.removeEventListener("abort", cancel); if (this.#pending === pending) this.#pending = undefined;