DSH / Atlas
2026-08-02implementedfeature

Win32 folder picker moves to koffi in a child process

Win32 文件夹选择器迁至 koffi 子进程

The Windows directory picker's primary tier was a spawned PowerShell script around WinForms `FolderBrowserDialog`: the modern dialog only where PowerShell 7 happens to be installed, a regression where PowerShell 6 resolves but has no WinForms (exit 1 is not `ENOENT`, so the 5.1 fallback never ran), a `SetProcessDPIAware` ceiling of system DPI, and a picker whose behavior depended on which shells a machine ships rathe

English

Problem

The Windows directory picker's primary tier was a spawned PowerShell script around WinForms FolderBrowserDialog: the modern dialog only where PowerShell 7 happens to be installed, a regression where PowerShell 6 resolves but has no WinForms (exit 1 is not ENOENT, so the 5.1 fallback never ran), a SetProcessDPIAware ceiling of system DPI, and a picker whose behavior depended on which shells a machine ships rather than on Windows itself.

Decision

packages/host/directory-picker-native now opens IFileOpenDialog (FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR) in-process through koffi — already a workspace dependency for the repo's other win32.ts surfaces — as the primary win32 tier. The COM conversation runs in a spawned child process so the modal Show never blocks the host event loop; the child posts its native thread id before blocking, and the driver services aborts by re-posting WM_CLOSE to that thread's windows (EnumThreadWindows), killing the child when the close budget is exhausted. The dialog is the child's first window, so Windows activates it without a foreground call. The child thread opts into the best thread DPI awareness the host accepts (SetThreadDpiAwarenessContext, cascading per-monitor-v2 → per-monitor → system-aware with the return value checked), a strict upgrade over the script's system-DPI ceiling; DPI stays a cosmetic best-effort — a host accepting none of them still gets the modern dialog rather than a downgrade. The module split keeps coverage honest on every host: win32-dialog-logic.ts (pure sequencing) and win32-dialog.ts (driver) test against fakes anywhere; win32-dialog-bindings.ts tests against a mocked koffi COM world (the dsh-session-persistence-jsonl technique); POSIX hosts run the real spawn plumbing to its koffi-load rejection; win32 hosts run a real open-and-abort-close smoke. The PowerShell chain that preceded this tier is gone (see the chain removal): the tier has no fallback.

Alternatives considered

  • A prebuilt native helper (native/ family like @deepseek-ai/node-addon-landlock-run). Rejected: another npm package family, MSVC provisioning, and a Windows build/release lane — all to ship ~150 lines of C the repository cannot currently exercise on CI (no real-Windows lane); koffi delivers the same COM surface with zero new supply chain.
  • An N-API in-process addon. Rejected for the same CI/toolchain reasons plus owned C++ for STA threading and message pumping that a child process + koffi express in TypeScript.
  • Keep PowerShell primary and probe versions. Rejected: the picker stays hostage to shell packaging (6 vs 7, Store aliases, profiles), and 5.1's legacy dialog remains the floor wherever pwsh is absent; the fallback-trigger widening alone was accepted into the fallback tier instead.
  • Blocking the main thread for the modal call. Rejected outright: the web host must keep serving RPC while the dialog is open.

Consequences

  • Every Windows machine gets the modern dialog with the best DPI awareness it supports (per-monitor-v2 on 1703+), PowerShell installed or not.
  • Real dialog rendering and the selection path stay a manual Windows check (the auto-close smoke proves open/abort/unwind).
  • The COM vtable slots and GUIDs used are frozen Windows ABI (Vista); a koffi signature mistake risks a native access violation, contained to the dialog child process — the host Node process survives and the failure surfaces as-is (no fallback tier; see the chain removal). The mocked-koffi ABI pins and the real win32 smoke exist to catch such mistakes before shipping.
  • The packaged-binary arm — the packaged executable spawning itself as the dialog entry — is not exercised by any automated test: the source plane and the built lib/worker.cjs under plain node are covered, and the packaged spawn remains deferred to the Windows CI roadmap.

中文

问题

Windows 目录选择器的主层此前是围绕 WinForms FolderBrowserDialog spawn 出的 PowerShell 脚本:只有恰好安装了 PowerShell 7 的机器才有现代对话框;一处回归——PowerShell 6 可解析却没有 WinForms(退出码 1 而非 ENOENT,5.1 回退永远不会触发);SetProcessDPIAware 只有系统 DPI 的上限;选择器的行为取决于机器装了哪些 shell,而不是取决于 Windows 本身。

决策

packages/host/directory-picker-native 现在经 koffi——它已是仓库其他 win32.ts 代码的工作区依赖——在进程内打开 IFileOpenDialogFOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR),作为 win32 主层。COM 会话运行在 spawn 出的子进程中,模态 Show 永不阻塞宿主事件循环;子进程在阻塞前上报其原生线程 id,驱动层通过向该线程的窗口反复投递 WM_CLOSEEnumThreadWindows)来处理中止请求,关闭等待预算耗尽后强制终止子进程。对话框是子进程的第一个窗口,Windows 会自动激活它,无需手动前台调用。子进程线程启用宿主接受的最佳线程 DPI 感知(SetThreadDpiAwarenessContext,按 per-monitor-v2 → per-monitor → system-aware 级联并检查返回值),严格优于脚本的系统 DPI 上限;DPI 保持为纯外观的 best-effort——不接受其中任何一种的宿主仍得到现代对话框,而不会降级。模块切分让覆盖率在任何主机上都诚实:win32-dialog-logic.ts(纯时序)与 win32-dialog.ts(driver)可在任何平台使用 fake 进行测试;win32-dialog-bindings.ts 对 mock 的 koffi COM 世界测试(dsh-session-persistence-jsonl 的技法);POSIX 主机运行真实的 spawn 管道,并验证其因 koffi 加载失败而拒绝;win32 主机运行真实的打开对话框并通过中止将其关闭的冒烟测试。先于本层存在的 PowerShell 链已被删除(见链删除):该层无回退。

考虑过的替代方案

  • 预编译原生辅助程序(native/ 家族,如 @deepseek-ai/node-addon-landlock-run)。 否决:再增加一个 npm 包家族、MSVC 环境配置和 Windows 构建/发布通道——只为交付约 150 行仓库目前无法通过 CI 检验的 C 代码(现有 CI 没有真 Windows 通道);koffi 以零新增供应链提供同一 COM 接口。
  • N-API 进程内插件。 否决:同样的 CI/工具链原因,还需自行维护处理 STA 线程与消息泵的 C++ 代码,而子进程 + koffi 用 TypeScript 就能表达。
  • 保留 PowerShell 为主层并探测版本。 否决:选择器仍被 shell 打包形态挟持(6 与 7、Store 别名、profile),且没有 pwsh 的机器仍只能使用 5.1 的旧版对话框;只有拓宽回退触发条件这一项改动被纳入了回退层。
  • 在主线程上阻塞模态调用。 直接否决:对话框打开期间 web 宿主必须继续服务 RPC。

后果

  • 每台 Windows 机器都得到带其所支持的最佳 DPI 感知(1703+ 为 per-monitor-v2)的现代对话框,无论是否安装 PowerShell。
  • 真实对话框的渲染与完成选择的流程仍需在 Windows 上手动检查(自动关闭冒烟测试证明打开/中止/收尾)。
  • 所用 COM vtable 槽位与 GUID 是冻结的 Windows ABI(Vista 起);koffi 签名错误可能引发原生访问冲突,但被限制在对话框子进程内——宿主 Node 进程存活,失败原样上报(无回退层;见链删除)。mocked-koffi 的 ABI 固定测试与真实 win32 冒烟测试正是为了在交付前捕获这类错误。
  • 打包二进制路径——打包后的可执行文件以对话框入口形式自我 spawn——不受任何自动化测试覆盖:源码侧与普通 node 下构建出的 lib/worker.cjs 已被覆盖,打包 spawn 推迟到 Windows CI 路线图。