feat(picker): open the Win32 folder dialog in-process over koffi

The modern IFileOpenDialog becomes the primary win32 tier: a koffi-driven
COM conversation on a worker_threads worker (the modal Show never blocks
the host event loop), per-monitor-v2 DPI via SetThreadDpiAwarenessContext,
and abort service by re-posting WM_CLOSE to the dialog thread's windows,
with terminate+unref as the last resort (Node cannot interrupt a thread
blocked in native code, and such a worker must never hold the process open).

The PowerShell chain stays as the fallback tier with its trigger widened
from ENOENT to any pwsh failure, closing the review-flagged PowerShell 6
regression (no WinForms: exit 1, not ENOENT, so 5.1 never ran).

Layering keeps per-file coverage honest on every host: pure sequencing and
the driver test against fakes anywhere; the bindings run against a mocked
koffi COM world (the session-persistence-jsonl technique); POSIX hosts
drive the real spawn plumbing to its koffi-load rejection; win32 hosts run
a real open-and-abort-close smoke. The smoke joins processBoundTests: a
worker blocked in a native modal wedges the threads pool's teardown, while
a fork contains it. The worker bundles as its own CJS tsdown entry
(workflow-workerthread's pattern; no TLA), and the host module is imported
statically so the node-half bundle stays chunk-free.

Built-plane and real-COM behavior verified on native Windows: standalone
probes for the source worker, the built CJS worker, and the driver's abort
path all open and close the real dialog.

Agent Notes: new implemented/feature/2026-08-02-win32-in-process-folder-dialog
(bilingual) owns the decision; the DPI note is re-scoped to the fallback tier
it now describes and its AutoUpgradeEnabled attribution corrected (.NET Core
3.0 rewrote FolderBrowserDialog; the opt-out arrived in .NET 6).
This commit is contained in:
Huanqi Cao
2026-08-03 00:06:47 +08:00
parent da1b1ff87d
commit 089f4dfad8
23 changed files with 1174 additions and 41 deletions

View File

@@ -1,6 +1,7 @@
/** Cross-platform native single-directory chooser behind the native backend's capability. */
import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command'
import { pickWin32Directory } from './win32-dialog.ts'
/** Testable command boundary; native implementations never invoke a shell. */
export type DirectoryPickerRunner = NativeCommandRunner
@@ -9,6 +10,8 @@ export type DirectoryPickerRunner = NativeCommandRunner
export interface DirectoryPickerInternals {
platform?: NodeJS.Platform
run?: DirectoryPickerRunner
/** Replaces the in-process Win32 dialog (`pickWin32Directory`) for deterministic tests. */
pickWin32Dialog?: (signal: AbortSignal) => Promise<string | null>
}
function outputPath(stdout: string): string | null {
@@ -64,13 +67,26 @@ export async function pickNativeDirectory(
}
if (platform === 'win32') {
// PowerShell 7 renders the modern IFileDialog folder picker, while Windows
// PowerShell 5.1's FolderBrowserDialog is hardwired to the legacy
// SHBrowseForFolder tree; prefer pwsh and fall back only when it is absent.
// Both hosts spawn DPI-unaware, so the script opts the process into system
// DPI awareness before any window is created. No Description is set: the
// modern dialog renders it as a bottom strip and the classic dialog as an
// unthemed box.
// Primary: the in-process koffi-backed IFileOpenDialog worker — the modern
// picker with per-monitor-v2 DPI, no PowerShell dependency, and abort
// support. Any non-abort failure (koffi unavailable, ancient Windows, COM
// refusal) falls back to the PowerShell chain below.
const pickDialog = internals.pickWin32Dialog ?? pickWin32Directory
try {
return await pickDialog(signal)
} catch (error: unknown) {
rethrowIfAborted(signal, error)
}
// PowerShell fallback: PowerShell 7 renders the modern IFileDialog folder
// picker, while Windows PowerShell 5.1's FolderBrowserDialog is hardwired
// to the legacy SHBrowseForFolder tree. Prefer pwsh, but ANY pwsh failure
// falls back to 5.1 (which every Windows ships): a resolvable pwsh can
// still be unable to deliver the dialog — PowerShell 6 has no WinForms,
// so its Add-Type exits 1, not ENOENT. Both hosts spawn DPI-unaware, so
// the script opts the process into system DPI awareness before any window
// is created. No Description is set: the modern dialog renders it as a
// bottom strip and the classic dialog as an unthemed box.
const script = [
"$ErrorActionPreference = 'Stop'",
"Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class DpiAware { [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); }'",
@@ -89,7 +105,6 @@ export async function pickNativeDirectory(
return outputPath(result.stdout)
} catch (error: unknown) {
rethrowIfAborted(signal, error)
if (!isMissingCommand(error)) throw error
}
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-Command', script], signal)
return outputPath(result.stdout)

View File

@@ -0,0 +1,157 @@
/**
* koffi-backed Win32 bindings for the folder dialog: the COM vtable calls
* behind {@link Win32DialogBindings} plus the cross-thread window closer the
* driver uses to service aborts. Loaded lazily and only on win32 (the dialog
* worker and the driver's abort path), so non-Windows processes never load
* koffi — the same containment as the repo's other `win32.ts` modules.
*
* The COM surface used here (IModalWindow/IFileDialog/IFileOpenDialog and
* IShellItem vtable order, the GUIDs, `FOS_*` and `SIGDN_FILESYSPATH`) is
* frozen Windows ABI since Vista; slots are offsets into the vtable at the
* object's first pointer.
*/
import type { Win32DialogBindings, Win32FolderDialog } from './win32-dialog-logic.ts'
interface KoffiFunction { (...args: unknown[]): unknown }
interface KoffiLibrary { func(convention: string, name: string, result: string, args: string[]): KoffiFunction }
interface Koffi {
load(path: string): KoffiLibrary
proto(declaration: string): unknown
pointer(type: unknown): unknown
call(pointer: unknown, proto: unknown, ...args: unknown[]): unknown
decode(value: unknown, offsetOrType: unknown, type?: unknown): unknown
register(fn: (...args: unknown[]) => unknown, type: unknown): unknown
unregister(callback: unknown): void
}
const COINIT_APARTMENTTHREADED = 0x2
const CLSCTX_INPROC_SERVER = 0x1
const SIGDN_FILESYSPATH = 0x80058000 | 0
const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
const WM_CLOSE = 0x10
/** IFileOpenDialog vtable slots (IUnknown 0-2, IModalWindow 3, IFileDialog 4+). */
const SLOT_RELEASE = 2
const SLOT_SHOW = 3
const SLOT_SET_OPTIONS = 9
const SLOT_SET_TITLE = 17
const SLOT_GET_RESULT = 20
/** IShellItem vtable slot for `GetDisplayName`. */
const SLOT_GET_DISPLAY_NAME = 5
/**
* Encode a canonical GUID string as its 16 little-endian bytes.
* @param text - the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` form.
* @returns the in-memory GUID bytes CoCreateInstance expects.
*/
function guidBytes(text: string): Buffer {
const match = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(text) as RegExpExecArray
const bytes = Buffer.alloc(16)
bytes.writeUInt32LE(parseInt(match[1] as string, 16), 0)
bytes.writeUInt16LE(parseInt(match[2] as string, 16), 4)
bytes.writeUInt16LE(parseInt(match[3] as string, 16), 6)
Buffer.from((match[4] as string) + (match[5] as string), 'hex').copy(bytes, 8)
return bytes
}
const CLSID_FILE_OPEN_DIALOG = guidBytes('dc1c5a9c-e88a-4dde-a5a1-60f82a20aef7')
const IID_IFILE_OPEN_DIALOG = guidBytes('d57c7288-d4ad-4768-be02-9d969532d960')
/**
* Load koffi and expose the dialog bindings for this thread.
* @returns the bindings {@link runFolderDialog} sequences against.
*/
export async function loadWin32DialogBindings(): Promise<Win32DialogBindings> {
const koffi = (await import('koffi')).default as unknown as Koffi
const ole32 = koffi.load('ole32.dll')
const user32 = koffi.load('user32.dll')
const kernel32 = koffi.load('kernel32.dll')
const coInitializeEx = ole32.func('__stdcall', 'CoInitializeEx', 'int32', ['void *', 'uint32'])
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', [])
const protoShow = koffi.proto('int32 __stdcall DshDialogShow(void *self, void *owner)')
const protoSetOptions = koffi.proto('int32 __stdcall DshDialogSetOptions(void *self, uint32 options)')
const protoSetTitle = koffi.proto('int32 __stdcall DshDialogSetTitle(void *self, str16 title)')
const protoGetResult = koffi.proto('int32 __stdcall DshDialogGetResult(void *self, _Out_ void **item)')
const protoGetDisplayName = koffi.proto('int32 __stdcall DshItemGetDisplayName(void *self, int32 form, _Out_ void **name)')
const protoRelease = koffi.proto('uint32 __stdcall DshComRelease(void *self)')
/** 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 *')
return (...args: unknown[]) => koffi.call(fn, proto, self, ...args) as number
}
return {
setThreadDpiAwareness: () => {
try {
const setThreadDpiAwarenessContext = user32.func('__stdcall', 'SetThreadDpiAwarenessContext', 'void *', ['intptr'])
setThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
} catch {
// SetThreadDpiAwarenessContext absent (Windows 10 pre-1703): the
// dialog renders at system DPI; nothing else can fail here because
// user32 itself loaded above.
}
},
coInitializeSta: () => coInitializeEx(null, COINIT_APARTMENTTHREADED) as number,
currentThreadId: () => getCurrentThreadId() as number,
createFolderDialog: (): Win32FolderDialog => {
const out = Buffer.alloc(8)
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 *')
return {
setOptions: options => method(dialog, SLOT_SET_OPTIONS, protoSetOptions)(options),
setTitle: title => method(dialog, SLOT_SET_TITLE, protoSetTitle)(title),
show: () => method(dialog, SLOT_SHOW, protoShow)(null),
resultPath: () => {
const itemOut: unknown[] = [null]
const gotItem = method(dialog, SLOT_GET_RESULT, protoGetResult)(itemOut)
if (gotItem < 0) return { hr: gotItem }
const item = itemOut[0]
try {
const nameOut: unknown[] = [null]
const gotName = method(item, SLOT_GET_DISPLAY_NAME, protoGetDisplayName)(SIGDN_FILESYSPATH, nameOut)
if (gotName < 0) return { hr: gotName }
const path = koffi.decode(nameOut[0], 'str16') as string
coTaskMemFree(nameOut[0])
return { hr: gotName, path }
} finally {
method(item, SLOT_RELEASE, protoRelease)()
}
},
release: () => {
method(dialog, SLOT_RELEASE, protoRelease)()
},
}
},
}
}
/**
* Post `WM_CLOSE` to every window of a native thread — the driver's abort
* lever against the worker blocked inside `Show`, after which `Show` returns
* `HRESULT_CANCELLED` and the worker unwinds normally.
* @param threadId - the dialog thread's native id (from the `showing` notice).
*/
export async function closeThreadWindows(threadId: number): Promise<void> {
const koffi = (await import('koffi')).default as unknown as Koffi
const user32 = koffi.load('user32.dll')
const enumThreadWindows = user32.func('__stdcall', 'EnumThreadWindows', 'int', ['uint32', 'void *', 'intptr'])
const postMessageW = user32.func('__stdcall', 'PostMessageW', 'int', ['void *', 'uint32', 'uintptr', 'intptr'])
const protoEnumProc = koffi.proto('int __stdcall DshEnumThreadWndProc(void *hwnd, intptr lparam)')
const callback = koffi.register((hwnd: unknown) => {
postMessageW(hwnd, WM_CLOSE, 0, 0)
return 1
}, koffi.pointer(protoEnumProc))
try {
enumThreadWindows(threadId, callback, 0)
} finally {
koffi.unregister(callback)
}
}

View File

@@ -0,0 +1,36 @@
/**
* Real-process half of the Win32 dialog driver: spawn the dialog worker
* (source or built plane) and close a dialog thread's windows. Loaded lazily
* and only on the win32 default path, so non-Windows processes never touch
* worker or koffi machinery; the driver's logic is tested against fakes of
* this surface instead.
*/
import { fileURLToPath } from 'node:url'
import { Worker } from 'node:worker_threads'
import type { Win32DialogWorkerData } from './win32-dialog-worker.ts'
/**
* Spawn the dialog worker. Built consumers load the bundled CJS worker next
* to this module; unbuilt (source) consumers bootstrap tsx inside the worker
* first, mirroring `dsh-workflow-workerthread`'s host.
* @param data - the worker payload (dialog title).
* @returns the spawned worker thread.
*/
export function spawnDialogWorker(data: Win32DialogWorkerData): Worker {
/* v8 ignore next 3 -- the built-output arm: tests always run unbuilt (src/) */
if (!import.meta.url.endsWith('.ts')) {
return new Worker(fileURLToPath(new URL('./win32-dialog-worker.cjs', import.meta.url)), { workerData: data })
}
const workerEntry = new URL('./win32-dialog-worker.ts', import.meta.url)
const bootstrap = [
`import { register as registerEsm } from ${JSON.stringify(import.meta.resolve('tsx/esm/api'))}`,
`import { register as registerCjs } from ${JSON.stringify(import.meta.resolve('tsx/cjs/api'))}`,
'registerCjs()',
'registerEsm()',
`await import(${JSON.stringify(workerEntry.href)})`,
].join('\n')
return new Worker(new URL(`data:text/javascript,${encodeURIComponent(bootstrap)}`), { workerData: data })
}
export { closeThreadWindows } from './win32-dialog-bindings.ts'

View File

@@ -0,0 +1,117 @@
/**
* Pure sequencing of the Win32 `IFileOpenDialog` folder-picker COM
* conversation over an injectable bindings seam, so every outcome path
* (selection, cancellation, HRESULT failure, cleanup ordering) is testable on
* any platform. The koffi-backed bindings live in
* `win32-dialog-bindings.ts`, which only a real win32 process ever loads.
*/
/** `HRESULT_FROM_WIN32(ERROR_CANCELLED)`: the user dismissed the dialog. */
export const HRESULT_CANCELLED = 0x800704c7 | 0
/** `FOS_PICKFOLDERS`: the dialog selects directories, not files. */
export const FOS_PICKFOLDERS = 0x20
/** `FOS_FORCEFILESYSTEM`: only results with a filesystem path can be chosen. */
export const FOS_FORCEFILESYSTEM = 0x40
/** `FOS_NOCHANGEDIR`: never mutate the process working directory. */
export const FOS_NOCHANGEDIR = 0x8
/** One created folder dialog: the vtable calls the sequencing needs. */
export interface Win32FolderDialog {
/**
* `IFileDialog::SetOptions`.
* @param options - the `FOS_*` flag union to apply.
* @returns the call's HRESULT.
*/
setOptions(options: number): number
/**
* `IFileDialog::SetTitle`.
* @param title - the dialog title text.
* @returns the call's HRESULT.
*/
setTitle(title: string): number
/**
* `IModalWindow::Show` with no owner window; blocks the calling thread
* until the user selects or dismisses.
* @returns the call's HRESULT (`HRESULT_CANCELLED` on dismissal).
*/
show(): number
/**
* `IFileDialog::GetResult` + `IShellItem::GetDisplayName(SIGDN_FILESYSPATH)`,
* releasing the shell item and freeing the COM string.
* @returns the call chain's HRESULT and, on success, the selected path.
*/
resultPath(): { hr: number; path?: string }
/** Release the dialog's COM reference. */
release(): void
}
/** The thread-level native surface the dialog sequencing runs against. */
export interface Win32DialogBindings {
/**
* Best-effort per-monitor-v2 DPI opt-in for the calling thread. Absent
* before Windows 10 1703; implementations swallow only that absence, so an
* old host merely renders the dialog at system DPI.
*/
setThreadDpiAwareness(): void
/**
* `CoInitializeEx(COINIT_APARTMENTTHREADED)` on the calling thread.
* @returns the call's HRESULT (`S_FALSE` re-entry is still a success).
*/
coInitializeSta(): number
/**
* `CoCreateInstance(CLSID_FileOpenDialog)`.
* @returns the created dialog surface; throws when creation fails.
*/
createFolderDialog(): Win32FolderDialog
/**
* `GetCurrentThreadId` — the native id a driver needs to close this
* thread's windows from outside.
* @returns the calling thread's native id.
*/
currentThreadId(): number
}
/**
* Throw when an HRESULT signals failure.
* @param hr - the HRESULT to check.
* @param what - the failing call's name for the error message.
* @returns the (successful) HRESULT unchanged.
*/
function check(hr: number, what: string): number {
if (hr < 0) throw new Error(`${what} failed: HRESULT 0x${(hr >>> 0).toString(16)}`)
return hr
}
/**
* Run one modal folder-picker conversation on the calling thread: DPI opt-in,
* STA init, dialog creation, `Show`, and result extraction, releasing the
* dialog on every path.
* @param bindings - the native surface (koffi-backed in production, fakes in tests).
* @param title - the dialog title text.
* @param onShowing - called with the native thread id immediately before the
* blocking `Show`, so a driver on another thread can close the dialog.
* @returns the selected filesystem path, or null when the user cancels.
*/
export function runFolderDialog(
bindings: Win32DialogBindings,
title: string,
onShowing: (threadId: number) => void,
): string | null {
bindings.setThreadDpiAwareness()
check(bindings.coInitializeSta(), 'CoInitializeEx')
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()
}
}

View File

@@ -0,0 +1,37 @@
/**
* Worker entry for the Win32 folder dialog: blocks THIS thread inside the
* modal `Show` so the host event loop stays live, reporting over the message
* port. Protocol: `{kind:'showing',threadId}` right before the blocking call
* (the driver's abort lever needs the native thread id), then exactly one of
* `{kind:'done',path}` or `{kind:'error',message}`.
*/
import { parentPort, workerData } from 'node:worker_threads'
import { loadWin32DialogBindings } from './win32-dialog-bindings.ts'
import { runFolderDialog } from './win32-dialog-logic.ts'
/** The driver-to-worker payload: the dialog title. */
export interface Win32DialogWorkerData { title: string }
/** One notice or outcome posted back to the driver. */
export type Win32DialogWorkerMessage =
| { kind: 'showing'; threadId: number }
| { kind: 'done'; path: string | null }
| { kind: 'error'; message: string }
const port = parentPort
if (port === null) throw new Error('win32-dialog-worker must run as a worker thread')
const { title } = workerData as Win32DialogWorkerData
// No top-level await: the built worker ships as CJS (pkg's VFS Worker hook
// compiles that format), which cannot carry TLA.
void (async () => {
try {
const bindings = await loadWin32DialogBindings()
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)
port.postMessage({ kind: 'error', message } satisfies Win32DialogWorkerMessage)
}
})()

View File

@@ -0,0 +1,128 @@
/**
* Main-thread driver for the Win32 folder dialog: spawns the dialog worker
* (which blocks inside the modal `Show`), maps its message protocol onto a
* promise, and services aborts by posting `WM_CLOSE` to the dialog thread's
* windows until the worker reports back. The real worker/window surface is
* injectable so every driver path is testable on any platform.
*/
import { closeThreadWindows as hostCloseThreadWindows, spawnDialogWorker } from './win32-dialog-host.ts'
import type { Win32DialogWorkerData, Win32DialogWorkerMessage } from './win32-dialog-worker.ts'
/** The worker surface the driver drives (satisfied by `node:worker_threads`). */
export interface Win32DialogWorkerLike {
/**
* Subscribe to a worker event.
* @param event - `message`, `error`, or `exit`.
* @param listener - the event consumer.
*/
on(event: 'message', listener: (message: Win32DialogWorkerMessage) => void): unknown
on(event: 'error', listener: (error: Error) => void): unknown
on(event: 'exit', listener: (code: number) => void): unknown
/**
* Force-stop the worker; the abort path's last resort when `WM_CLOSE`
* never lands (e.g. the dialog window was never created).
* @returns settles when the thread is gone.
*/
terminate(): Promise<number>
/**
* Release the event-loop reference. Called once the pick settles so a
* worker stuck in the native modal call (terminate cannot interrupt
* native code) never blocks process exit.
*/
unref?(): void
}
/** Injectable process surface for deterministic driver tests. */
export interface Win32DialogInternals {
/** Replaces the real worker spawn (`win32-dialog-host.ts`). */
spawnWorker?: (data: Win32DialogWorkerData) => Win32DialogWorkerLike
/** Replaces the real `WM_CLOSE` poster (`win32-dialog-host.ts`). */
closeThreadWindows?: (threadId: number) => Promise<void>
/** Abort-service cadence override so tests never wait wall-clock time. */
closeRetryMs?: number
}
/** The dialog title every host shows. */
export const DIALOG_TITLE = 'Select Workspace Directory'
/** `WM_CLOSE` re-post cadence while an abort waits for the worker to unwind. */
const CLOSE_RETRY_MS = 150
/** Abort-service attempts before force-terminating the worker. */
const CLOSE_MAX_ATTEMPTS = 20
/**
* Open the modern Win32 folder picker off the event loop.
* @param signal - caller lifetime; abort closes the dialog and rejects.
* @param internals - worker/window seams for deterministic tests.
* @returns the selected path, or null when the user cancels.
*/
export async function pickWin32Directory(
signal: AbortSignal,
internals: Win32DialogInternals = {},
): Promise<string | null> {
if (signal.aborted) throw new Error('native directory picker aborted')
const spawnWorker = internals.spawnWorker ?? spawnDialogWorker
const closeWindows = internals.closeThreadWindows ?? hostCloseThreadWindows
const closeRetryMs = internals.closeRetryMs ?? CLOSE_RETRY_MS
const worker = spawnWorker({ title: DIALOG_TITLE })
let dialogThreadId: number | undefined
let closeTimer: NodeJS.Timeout | undefined
let settled = false
return await new Promise<string | null>((resolve, reject) => {
const settle = (outcome: () => void): void => {
if (settled) return
settled = true
if (closeTimer !== undefined) clearInterval(closeTimer)
signal.removeEventListener('abort', onAbort)
worker.unref?.()
outcome()
}
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.
closeTimer = setInterval(() => {
attempts += 1
if (attempts > CLOSE_MAX_ATTEMPTS) {
settle(() => {
void worker.terminate()
reject(new Error('native directory picker aborted (dialog unresponsive; worker terminated)'))
})
return
}
void closeWindows(dialogThreadId as number).catch(() => undefined)
}, closeRetryMs)
void closeWindows(dialogThreadId as number).catch(() => undefined)
}
const onAbort = (): void => {
if (dialogThreadId !== undefined) serviceAbort()
// Not shown yet: the `showing` handler below starts the service loop.
}
signal.addEventListener('abort', onAbort, { once: true })
worker.on('message', (message: Win32DialogWorkerMessage) => {
switch (message.kind) {
case 'showing':
dialogThreadId = message.threadId
if (signal.aborted) serviceAbort()
return
case 'done':
settle(() => {
if (signal.aborted) reject(new Error('native directory picker aborted'))
else resolve(message.path)
})
return
case 'error':
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')) }) })
})
}