fix(picker): pointer-width vtable offsets, COM apartment pairing, unconditional abort budget, and the full failure chain

Review round two on the in-process dialog:

- Vtable slots and out-pointers use koffi.sizeof('void *') instead of a
  hardcoded 8 - win32-ia32 (which Node and koffi both ship) would have read
  method pointers from the wrong address and crashed in-process before any
  fallback could run.
- runFolderDialog pairs every successful (incl. S_FALSE) CoInitializeEx
  with CoUninitialize in the outermost finally, releasing the dialog first;
  a failed init is deliberately unpaired. Pinned across fake-bindings and
  mocked-koffi suites.
- The abort close budget starts unconditionally: a worker hung before the
  showing notice (koffi import or COM init) now ends in terminate instead
  of a dangling promise; WM_CLOSE posting still waits for the thread id.
- A triple miss (dialog + pwsh + 5.1) surfaces an AggregateError carrying
  all three causes - the in-process tier's reason was previously
  unrecoverable from the final PowerShell error.
- The stray '=>{ ' formatter artifacts are normalized to real blocks.

Both stale note claims from the review are fixed: the DPI note's
Consequences no longer claims an ENOENT classification or zero new
dependencies, and the 2026-07-27 picker note's Windows bullet now names
the in-process primary and keeps the PowerShell chain as fallback (both
languages, pairings re-recorded).
This commit is contained in:
Huanqi Cao
2026-08-03 20:40:10 +08:00
parent fed3149ac4
commit 8500a21658
15 changed files with 147 additions and 50 deletions

View File

@@ -72,10 +72,12 @@ export async function pickNativeDirectory(
// support. Any non-abort failure (koffi unavailable, ancient Windows, COM
// refusal) falls back to the PowerShell chain below.
const pickDialog = internals.pickWin32Dialog ?? pickWin32Directory
let dialogError: unknown
try {
return await pickDialog(signal)
} catch (error: unknown) {
rethrowIfAborted(signal, error)
dialogError = error
}
// PowerShell fallback: PowerShell 7 renders the modern IFileDialog folder
@@ -100,14 +102,27 @@ export async function pickNativeDirectory(
' [Console]::WriteLine($dialog.SelectedPath)',
'}',
].join('; ')
let pwshError: unknown
try {
const result = await run('pwsh.exe', ['-NoProfile', '-STA', '-Command', script], signal)
return outputPath(result.stdout)
} catch (error: unknown) {
rethrowIfAborted(signal, error)
pwshError = error
}
try {
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-Command', script], signal)
return outputPath(result.stdout)
} catch (error: unknown) {
rethrowIfAborted(signal, error)
// Triple miss: every tier failed. Surface all three causes — the
// in-process dialog's reason is otherwise unrecoverable from the last
// PowerShell error alone.
throw new AggregateError(
[dialogError, pwshError, error],
'native directory picker failed: the in-process dialog and both PowerShell hosts failed',
)
}
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-Command', script], signal)
return outputPath(result.stdout)
}
if (platform === 'linux') {

View File

@@ -23,6 +23,7 @@ interface Koffi {
decode(value: unknown, offsetOrType: unknown, type?: unknown): unknown
register(fn: (...args: unknown[]) => unknown, type: unknown): unknown
unregister(callback: unknown): void
sizeof(type: string): number
}
const COINIT_APARTMENTTHREADED = 0x2
@@ -68,7 +69,11 @@ export async function loadWin32DialogBindings(): Promise<Win32DialogBindings> {
const user32 = koffi.load('user32.dll')
const kernel32 = koffi.load('kernel32.dll')
// Vtable slots and out-pointers are pointer-width offsets: 8 on x64/arm64,
// 4 on ia32 — koffi reports the running process's width.
const pointerSize = koffi.sizeof('void *')
const coInitializeEx = ole32.func('__stdcall', 'CoInitializeEx', 'int32', ['void *', 'uint32'])
const coUninitialize = ole32.func('__stdcall', 'CoUninitialize', 'void', [])
const coCreateInstance = ole32.func('__stdcall', 'CoCreateInstance', 'int32', ['void *', 'void *', 'uint32', 'void *', 'void *'])
const coTaskMemFree = ole32.func('__stdcall', 'CoTaskMemFree', 'void', ['void *'])
const getCurrentThreadId = kernel32.func('__stdcall', 'GetCurrentThreadId', 'uint32', [])
@@ -83,7 +88,7 @@ export async function loadWin32DialogBindings(): Promise<Win32DialogBindings> {
/** Bind vtable slot `slot` of COM object `self` to a caller through `proto`. */
const method = (self: unknown, slot: number, proto: unknown): (...args: unknown[]) => number => {
const vtable = koffi.decode(self, 'void *')
const fn = koffi.decode(vtable, slot * 8, 'void *')
const fn = koffi.decode(vtable, slot * pointerSize, 'void *')
return (...args: unknown[]) => koffi.call(fn, proto, self, ...args) as number
}
@@ -99,9 +104,12 @@ export async function loadWin32DialogBindings(): Promise<Win32DialogBindings> {
}
},
coInitializeSta: () => coInitializeEx(null, COINIT_APARTMENTTHREADED) as number,
coUninitialize: () => {
coUninitialize()
},
currentThreadId: () => getCurrentThreadId() as number,
createFolderDialog: (): Win32FolderDialog => {
const out = Buffer.alloc(8)
const out = Buffer.alloc(pointerSize)
const created = coCreateInstance(CLSID_FILE_OPEN_DIALOG, null, CLSCTX_INPROC_SERVER, IID_IFILE_OPEN_DIALOG, out) as number
if (created < 0) throw new Error(`CoCreateInstance(FileOpenDialog) failed: HRESULT 0x${(created >>> 0).toString(16)}`)
const dialog = koffi.decode(out, 'void *')

View File

@@ -59,6 +59,12 @@ export interface Win32DialogBindings {
* @returns the call's HRESULT (`S_FALSE` re-entry is still a success).
*/
coInitializeSta(): number
/**
* `CoUninitialize` on the calling thread — COM requires one pairing call
* for every successful (including `S_FALSE`) `CoInitializeEx`, even on a
* thread that exits right after the conversation.
*/
coUninitialize(): void
/**
* `CoCreateInstance(CLSID_FileOpenDialog)`.
* @returns the created dialog surface; throws when creation fails.
@@ -100,18 +106,24 @@ export function runFolderDialog(
): string | null {
bindings.setThreadDpiAwareness()
check(bindings.coInitializeSta(), 'CoInitializeEx')
const dialog = bindings.createFolderDialog()
// From here the apartment is initialized (S_OK or S_FALSE) and must be
// uninitialized exactly once on every path.
try {
check(dialog.setOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR), 'SetOptions')
check(dialog.setTitle(title), 'SetTitle')
onShowing(bindings.currentThreadId())
const shown = dialog.show()
if (shown === HRESULT_CANCELLED) return null
check(shown, 'Show')
const result = dialog.resultPath()
check(result.hr, 'GetResult')
return result.path as string
const dialog = bindings.createFolderDialog()
try {
check(dialog.setOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR), 'SetOptions')
check(dialog.setTitle(title), 'SetTitle')
onShowing(bindings.currentThreadId())
const shown = dialog.show()
if (shown === HRESULT_CANCELLED) return null
check(shown, 'Show')
const result = dialog.resultPath()
check(result.hr, 'GetResult')
return result.path as string
} finally {
dialog.release()
}
} finally {
dialog.release()
bindings.coUninitialize()
}
}

View File

@@ -28,7 +28,9 @@ const { title } = workerData as Win32DialogWorkerData
void (async () => {
try {
const bindings = await loadWin32DialogBindings()
const path = runFolderDialog(bindings, title, (threadId) =>{ port.postMessage({ kind: 'showing', threadId } satisfies Win32DialogWorkerMessage) })
const path = runFolderDialog(bindings, title, (threadId) => {
port.postMessage({ kind: 'showing', threadId } satisfies Win32DialogWorkerMessage)
})
port.postMessage({ kind: 'done', path } satisfies Win32DialogWorkerMessage)
} catch (error: unknown) {
const message = error instanceof Error ? (error.stack ?? error.message) : String(error)

View File

@@ -81,11 +81,20 @@ export async function pickWin32Directory(
outcome()
}
const postClose = (): void => {
// Before `showing` there is no window to close; the budget below still
// runs so a worker that never reports cannot dangle the pick.
if (dialogThreadId !== undefined) void closeWindows(dialogThreadId).catch(() => undefined)
}
// Sole caller: the once-registered abort listener, so no re-entry guard.
const serviceAbort = (): void => {
let attempts = 0
// The `showing` notice precedes the blocking `Show`, so the very first
// WM_CLOSE can race the window's creation; re-post until the worker
// reports back, then force-terminate as a last resort.
// reports back, then force-terminate as a last resort. The budget is
// unconditional — an abort before `showing` (worker hung in koffi or
// COM init) still ends in terminate instead of a dangling promise.
closeTimer = setInterval(() => {
attempts += 1
if (attempts > CLOSE_MAX_ATTEMPTS) {
@@ -95,14 +104,13 @@ export async function pickWin32Directory(
})
return
}
void closeWindows(dialogThreadId as number).catch(() => undefined)
postClose()
}, closeRetryMs)
void closeWindows(dialogThreadId as number).catch(() => undefined)
postClose()
}
const onAbort = (): void => {
if (dialogThreadId !== undefined) serviceAbort()
// Not shown yet: the `showing` handler below starts the service loop.
serviceAbort()
}
signal.addEventListener('abort', onAbort, { once: true })
@@ -110,7 +118,8 @@ export async function pickWin32Directory(
switch (message.kind) {
case 'showing':
dialogThreadId = message.threadId
if (signal.aborted) serviceAbort()
// An abort that raced ahead of this notice now has a window to hit.
if (signal.aborted) postClose()
return
case 'done':
settle(() => {
@@ -119,10 +128,20 @@ export async function pickWin32Directory(
})
return
case 'error':
settle(() =>{ reject(new Error(`win32 folder dialog failed: ${message.message}`)) })
settle(() => {
reject(new Error(`win32 folder dialog failed: ${message.message}`))
})
}
})
worker.on('error', (error: Error) =>{ settle(() =>{ reject(error) }) })
worker.on('exit', () =>{ settle(() =>{ reject(new Error('win32 folder dialog worker exited before reporting a result')) }) })
worker.on('error', (error: Error) => {
settle(() => {
reject(error)
})
})
worker.on('exit', () => {
settle(() => {
reject(new Error('win32 folder dialog worker exited before reporting a result'))
})
})
})
}