From 5f9ac2c98a6fe11988c41afb737df9b6d5c3a1b3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:33:12 +0800 Subject: [PATCH] fix(windows): retain primary snapshot and cancellation failures Refs #4502, #4504. Keep the original snapshot failure alongside disposal uncertainty and retain exact cancellation reasons at the TypeScript runtime boundary. Native Windows execution and addon fault behavior were not verified on this macOS host; checked handle-release ownership in #4505 remains open. --- native/windows/src/filesystem.cc | 7 +++--- src/windows/WindowsPrivateRuntime.test.ts | 27 +++++++++++++++++++++++ src/windows/WindowsPrivateRuntime.ts | 23 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) 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;