PowerToys File Locksmith 实现详解:双 Shell 扩展、文件式 IPC 与基于 Nt API 的文件锁扫描
【免费下载链接】PowerToysMicrosoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows项目地址: https://gitcode.com/GitHub_Trending/po/PowerToys
本文基于 PowerToys 仓库中的 File Locksmith 开发文档,完整梳理该模块"识别并解除文件锁"的实现路径:从 Windows 10/11 双 Shell 扩展的注册与调用,到基于文件写作的 IPC 机制,再到利用NtQuerySystemInformation枚举系统级文件句柄的核心扫描算法,并覆盖 WinUI 3 界面的异步加载、设置集成与完整的本地调试流程。读完后你可以理解一个 Windows Shell 扩展模块从右键菜单点击到进程列表呈现的全链路调用关系。
模块定位与整体架构
File Locksmith 是 PowerToys 中的一个实用工具,用于显示哪些进程正在锁定或占用某个特定文件。当用户无法删除、移动或修改某个文件时,通过它可以看到持有该文件句柄的进程列表,并直接在界面上结束对应进程,从而"解锁"文件。
从文档给出的架构看,File Locksmith 与 ImageResizer、NewPlus 模块采用类似的"Shell 扩展 + 独立 UI 进程"分层结构,由三部分组成:
- Shell Extensions(Shell 扩展)
FileLocksmithExt:面向 Windows 10 及更早版本的 COM Shell 扩展,在文件资源管理器右键菜单中注入 "Unlock with File Locksmith" 项;FileLocksmithContextMenu:面向 Windows 11 新上下文菜单的 Shell 扩展,以 MSIX 稀疏包(sparse package)形式注册。
- Core Components(核心组件)
FileLocksmithLib:负责 Shell 扩展与 UI 之间的进程间通信(IPC)及模块设置读取;FileLocksmithLibInterop:核心功能库,负责扫描锁定目标文件的进程;FileLocksmithUI:WinUI 3 用户界面进程PowerToys.FileLocksmithUI.exe。
- Settings Integration(设置集成):PowerToys 设置应用中的模块开关与属性项。
各源码目录的实际落点为 FileLocksmithExt、FileLocksmithContextMenu、FileLocksmithLib、FileLocksmithLibInterop 与 FileLocksmithUI,与架构图中的方框一一对应。
双 Shell 扩展:Windows 10 的 COM 对象与 Windows 11 的 MSIX 命令
模块在资源管理器右键菜单中添加 "Unlock with File Locksmith",但针对两个 Windows 版本采用了两套注册方式:
- Windows 11:上下文菜单命令以 MSIX 稀疏包注册,命令定义编译自 AppxManifest.xml,安装包名常量
ContextMenuPackageName = L"FileLocksmithContextMenu"定义在 Constants.h 中; - Windows 10 及以下:传统 COM Shell 扩展,安装时通过注册表键注册(安装包对应 installer/FileLocksmith.wxs),运行时由 Explorer 加载 DLL。
Windows 10 路径:FileLocksmithExt 的 COM 实现
ExplorerCommand.cpp 中的ExplorerCommand类同时实现IExplorerCommand、IShellExtInit和IContextMenu三个接口。关键行为:
QueryContextMenu(约 L113-L170):先检查模块是否启用(FileLocksmithSettingsInstance().GetEnabled()),未启用直接返回E_FAIL;再根据GetShowInExtendedContextMenu()决定是否仅在"显示更多选项"扩展菜单(CMF_EXTENDEDVERBS)中显示;菜单项带有从 DLL 资源加载的 16×16 图标,插入失败时通过 ETW(Trace::QueryContextMenuError)记录错误。GetState:模块禁用时返回ECS_HIDDEN,使菜单项隐藏而不是置灰。InvokeCommand(L172-L235):这是整个链路的触发点。它先创建ipc::Writer打开 IPC 文件,然后调用LaunchUI用CreateProcessW拉起"PowerToys.FileLocksmithUI.exe"(路径为扩展 DLL 所在目录 + Constants.h 中的FileNameUIExe),随后遍历IDataObject中的每个选中项,用SIGDN_FILESYSPATH取出文件系统路径并逐条writer.add_path写入 IPC 文件。
模块其余文件:ClassFactory.cpp(COM 类工厂)、PowerToysModule.cpp(PowerToys 模块接口与设置管理)、dllmain.cpp(DLL 入口点)。
Windows 11 路径:FileLocksmithContextMenu 的 MSIX 命令
FileLocksmithContextMenu/dllmain.cpp 定义了FileLocksmithContextMenuCommand类(CoCreatableClass,UUID 为AAF1E27D-4976-49C2-8895-AAFA743C0A7E),实现IExplorerCommand与IObjectWithSite:
GetState:模块禁用或开启了扩展菜单选项时返回ECS_HIDDEN;Invoke(约 L94-L159):与 Win10 路径逻辑一致——先writer.start()建立 IPC 文件,再遍历IShellItemArray选择项写入路径。一个值得注意的差异是,它通过RunNonElevatedEx以非提升权限启动 UI(配合 common/utils/elevation.h),即使命令来自提权的 Explorer 进程也是如此;- 图标不再从 DLL 资源加载,而是指向 MSIX 包内资源路径
Assets\FileLocksmith\FileLocksmith.ico。
文件式 IPC:右键菜单如何把文件路径交给 UI
文档描述的进程通信流程为:
- 用户在 PowerToys 设置中启用 File Locksmith;
- 用户右键点击文件,选择 "Unlock with File Locksmith";
- Shell 扩展把选中的文件路径写入一个共享文件(file-based IPC);
- Shell 扩展启动
PowerToys.FileLocksmithUI.exe; - UI 从共享文件中读出路径列表;
- UI 调用
FileLocksmithLibInterop扫描持有该文件句柄的进程; - 结果在 UI 中呈现,用户可查看详情并结束进程。
结合源码可以补全更多细节。IPC.cpp 中的ipc::Writer:
start()用PTSettingsHelper::get_module_save_folder_location(L"File Locksmith")取得模块保存目录,在其下打开名为last-run.log的文件(常量LastRunPath = L"\\last-run-path"对应L"\\last-run.log",见 Constants.h)——也就是说,这个"临时文件"实际落在模块的专用数据目录中,而非系统 Temp 目录;add_path()把每个路径以 UTF-16 编码写入,并以换行符结尾,支持一次右键选择多个文件;finish()追加一个空路径行作为结束标记,然后关闭流。
UI 侧的读取与扫描入口在 MainViewModel.cs:构造函数中调用NativeMethods.ReadPathsFromFile()读回路径,LoadProcessesAsync在后台线程执行NativeMethods.FindProcessesRecursive(paths)。跨语言桥接由 NativeMethods.cpp(C++/WinRT 互操作层)完成。
模块的全部关键常量集中在 Constants.h:
| 常量 | 值 | 用途 |
|---|---|---|
FileNameUIExe | PowerToys.FileLocksmithUI.exe | UI 可执行文件名 |
JsonKeyEnabled | Enabled | 设置 JSON 中"启用"键 |
JsonKeyShowInExtendedContextMenu | showInExtendedContextMenu | 是否仅显示在扩展上下文菜单 |
DataFilePath | \file-locksmith-settings.json | 模块设置 JSON 文件路径 |
LastRunPath | \last-run.log | IPC 共享文件名 |
ContextMenuPackageName | FileLocksmithContextMenu | Win11 上下文菜单 MSIX 包名 |
值得一提的是,IPC.cpp 顶部已经定义了DefaultPipeBufferSize = 8192与DefaultPipeTimeoutMillis = 200两个管道相关常量,但当前Writer仍完全基于文件流实现。从源码结构看,这与文档"已知问题"一节描述的"将 IPC 从文件式改为管道式"的重构方向相互印证——管道改造在代码层面已有铺垫,但尚未落地。
核心扫描实现:用 NtQuerySystemInformation 枚举全系统文件句柄
"找出谁锁住了文件"的核心逻辑位于 FileLocksmith.cpp 的find_processes_recursive(L18-L113),它通过 NtdllExtensions.h 封装的 NT 底层 API 工作。整体算法分四步:
第 1 步:把普通路径转换成内核名。Windows 内核对象使用形如\Device\HarddiskVolume2\...的路径表示文件,与用户可见的C:\...不同。nt_ext.path_to_kernel_name(path)完成这一转换,并依据GetFileAttributesW判断目标是文件还是目录,分别放入kernel_names_files和kernel_names_dirs两个映射。
第 2 步:匹配规则支持目录递归。kernel_paths_containlambda 实现了三级匹配:
- 与文件内核名的精确相等;
- 与目录内核名的精确相等(锁定了目录本身);
- 前缀匹配:句柄路径以"目录内核名 + 反斜杠"开头时视为命中,并还原为普通路径返回。这就是函数名中 "recursive" 的含义——对目录做右键时,其下任意深度的子文件句柄都会被捕获。
第 3 步:遍历全系统句柄。nt_ext.handles()枚举系统中所有进程的句柄,只保留type_name == L"File"的条目,把命中者按 PID 归入pid_files集合。
第 4 步:同时检查进程的已加载模块。一个.exe/.dll被加载后即使没有显式句柄也处于"占用"状态,因此代码还会遍历nt_ext.processes()返回的每个进程的modules列表,将加载模块路径也纳入匹配。最后把 PID 与进程名、用户、命中文件列表组装为ProcessResult返回。
底层查询封装在 NtdllExtensions.cpp / NtdllBase.cpp 中,从源码结构看其工作机制为:
- 使用
NtQuerySystemInformation,信息类别为SystemExtendedHandleInformation(值 64)获取全系统句柄表,再用ObjectNameInformation(值 1)解析 File 句柄对应的内核对象名; - 结果缓冲区从
DefaultResultBufferSize = 64 KB起步,按需要扩容,上限MaxResultBufferSize = 1 GB——这是应对句柄数量大的系统的循环扩容策略; - 进程名、用户信息(
pid_to_user)与模块列表一并填充进ProcessInfo结构。
此外,FileLocksmith.cpp 还实现了pid_to_full_path(L117-L129):以PROCESS_QUERY_INFORMATION | PROCESS_VM_READ打开进程,用GetModuleFileNameExW取主模块全路径,缓冲区按LongMaxPathSize = 65536预留,供 UI 展示进程可执行文件位置。进程信息的容器类见 ProcessResult.cpp,存储名称、PID、用户与文件列表四元组。
WinUI 3 界面:异步加载、进程监视与提权重开
UI 由 App.xaml.cs 启动,入口处的两个前置处理值得注意:
- GPO 检查:
GPOWrapper.GetConfiguredFileLocksmithEnabledValue()若返回Disabled(组策略强制禁用),记录警告日志并直接Environment.Exit(0)——管理员可以通过策略彻底关闭该工具; - 调试权限:若进程已提权(
IsProcessElevated()),调用SetDebugPrivilege()获得 DEBUG 特权,否则将无法枚举系统级进程的句柄,日志会提示 "Couldn't set debug privileges to see system processes"。
主窗口逻辑在 MainViewModel.cs,采用 MVVM(CommunityToolkit.Mvvm)组织:
LoadProcessesCommand:异步命令,先Process.WaitForExitAsync等待后台扫描,把结果逐条加入Processes集合,并对每个进程挂起监视任务;WatchProcess(L122-L146):对每个发现的进程调用Process.WaitForExitAsync,进程一旦退出即自动从列表移除——用户结束进程后界面会即时更新,无需手动刷新;EndTask(L148-L168):[RelayCommand]方法,Process.Kill()结束选中进程,失败时记录错误并从列表移除;RestartElevated(L170-L183):当扫描权限不足时,通过NativeMethods.StartAsElevated(paths)把同样的路径列表重新以管理员身份启动 UI,实现"提权重开"。
界面数据到视觉元素的转换由四个 Converter 承担,均在 FileLocksmithUI/Converters 目录下:
- FileCountConverter.cs:文件数量显示;
- FileListToDescriptionConverter.cs:文件列表格式化;
- PidToIconConverter.cs:按 PID 提取进程图标;
- UserToSystemWarningVisibilityConverter.cs:对系统进程(如 SYSTEM 用户)显示警告提示。
其余 UI 文件:MainWindow.xaml.cs(主窗口)、ResourceLoaderInstance.cs(本地化资源辅助,语言加载逻辑在App构造函数中通过LanguageHelper.LoadLanguage()生效)。日志初始化到模块目录下的File Locksmith\FileLocksmithUI\Logs。
设置集成与配置文件
File Locksmith 的设置分 C++ 运行时与设置 UI 两层:
- C++ 侧:Shell 扩展与 UI 都通过 Settings.cpp(
FileLocksmithSettingsInstance())读取Enabled与showInExtendedContextMenu两个布尔项,数据文件为 Constants.h 中定义的\file-locksmith-settings.json; - 设置 UI 侧:FileLocksmithViewModel.cs 提供设置页视图模型,FileLocksmithProperties.cs 存用户级设置、FileLocksmithLocalProperties.cs 存机器级设置、FileLocksmithSettings.cs 定义模块设置项。
开关的即时生效机制体现在 Shell 扩展上:QueryContextMenu/GetState每次菜单构建时都会重新读取设置,关闭模块后菜单项立即隐藏,无需重启 Explorer。
构建与调试
调试流程分为"构建"与"部署 MSIX"两阶段,完整步骤继承自开发文档:
0. 构建模块
- 关闭现有 PowerToys 发布版进程;
- 在 Visual Studio 中打开解决方案,先整体构建,再单独构建
FileLocksmith项目。
1. 创建并导入自签名证书(若尚未创建)
New-SelfSignedCertificate -Subject "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" ` -KeyUsage DigitalSignature ` -Type CodeSigningCert ` -FriendlyName "PowerToys SelfCodeSigning" ` -CertStoreLocation "Cert:\CurrentUser\My" $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.FriendlyName -like "*PowerToys*" } Export-Certificate -Cert $cert -FilePath "$env:TEMP\PowerToysCodeSigning.cer" # 在管理员终端执行: Import-Certificate -FilePath "$env:TEMP\PowerToysCodeSigning.cer" -CertStoreLocation Cert:\LocalMachine\Root # 获取 Thumbprint Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.FriendlyName -like "*PowerToys*" }2. 对 MSIX 包签名
SignTool sign /fd SHA256 /sha1 <CERTIFICATE_THUMBPRINT> "C:\Users\$env:USERNAME\source\repos\PowerToys\x64\Debug\WinUI3Apps\FileLocksmithContextMenuPackage.msix"SignTool 可能不在 PATH 中,需指定完整路径,例如C:\Program Files (x86)\Windows Kits\10\bin\<version>\x64\signtool.exe。文档给出的完整示例:
PS C:\Users\developer> New-SelfSignedCertificate -Subject "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" ` >> -KeyUsage DigitalSignature ` >> -Type CodeSigningCert ` >> -FriendlyName "PowerToys SelfSigned" ` >> -CertStoreLocation "Cert:\CurrentUser\My" PS C:\Users\developer> Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.FriendlyName -like "*PowerToys*" } PS C:\Users\developer> & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe" sign /fd SHA256 /sha1 1AA018C2B06B60EAFEE452ADE403306F39058FF5 "%REPO_PATH%\PowerToys\x64\Debug\WinUI3Apps\FileLocksmithContextMenuPackage.msix" Done Adding Additional Store Successfully signed: C:\Users\developer\Develop\GitHub\PowerToys\x64\Debug\WinUI3Apps\FileLocksmithContextMenuPackage.msix3. 移除旧版本
Get-AppxPackage -Name Microsoft.PowerToys.FileLocksmithContextMenu* Remove-AppxPackage Microsoft.PowerToys.FileLocksmithContextMenu_1.0.0.0_neutral__8wekyb3d8bbwe4. 安装新签名的 MSIX
Add-AppxPackage -Path "%REPO_PATH%\PowerToys\x64\Debug\WinUI3Apps\FileLocksmithContextMenuPackage.msix" -ExternalLocation "%REPO_PATH%\PowerToys\x64\Debug\WinUI3Apps"5. 重启 Explorer:在任务管理器中重启explorer.exe,使新注册的上下文菜单命令生效。
6. 附加调试器
- 在 FileLocksmithContextMenu/dllmain.cpp 的
Invoke附近设置断点; - 打开 Visual Studio 的"附加到进程"对话框,在资源管理器中右键一个文件,附加到标题带FileLocksmith的
dllhost.exe进程以调试 Shell 扩展; - 再次快速右键文件并选择 "Unlock with File Locksmith",然后改为附加到
PowerToys.FileLocksmithUI.exe调试 UI 端。
7. 替代调试方式:直接把FileLocksmithUI设为启动项目,跳过 Shell 扩展直接拉起 UI,适合专注调试界面逻辑。
测试覆盖与已知问题
仓库中与该模块相关的测试包括:
- UI 测试:FileLocksmithContextMenuTests.cs(验证右键菜单行为)、FileLocksmithProcessListTests.cs(验证进程列表),以及辅助类 ExplorerHelper.cs 与 FileLocksmithTestHelper.cs;
- 命令行单元测试:仓库中还存在 FileLocksmithCLI 项目(main.cpp、CLILogic.cpp),配套 FileLocksmithCLITests.cpp,从源码结构看是同核心功能的命令行形态。
已知问题(继承自文档):将一个 IPC 机制从文件式改为管道式的 PR 仍处于受阻状态,阻塞点为:
- 以管理员身份重启时,上下文菜单扩展不显示;
- 以管理员身份启动时,"Unlock with File Locksmith" 选项不工作。
这与源码中的两处防御性设计相呼应:Win11 扩展特意用RunNonElevatedEx强制非提权启动 UI,而 UI 端又提供RestartElevated提权重开——提权场景下的行为目前正是该模块最复杂、也最需要留意的边界情况。
小结
File Locksmith 展示了 PowerToys 中一类典型的"Shell 扩展 + 独立 UI"模块范式:两套针对不同 Windows 版本的菜单入口、基于模块数据目录下last-run.log的轻量文件式 IPC、以及依赖NtQuerySystemInformation(SystemExtendedHandleInformation+ObjectNameInformation)的全系统句柄枚举算法。理解这条从ExplorerCommand::InvokeCommand到find_processes_recursive再到MainViewModel.LoadProcessesAsync的完整调用链,即可复用到 PowerToys 其他同类模块的二次开发与调试中。
【免费下载链接】PowerToysMicrosoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows项目地址: https://gitcode.com/GitHub_Trending/po/PowerToys
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考