首页
/ Microsoft PowerToys 更新机制全解:从 GitHub 版本检测到两阶段自动安装的完整链路

Microsoft PowerToys 更新机制全解:从 GitHub 版本检测到两阶段自动安装的完整链路

2026-09-06 15:46:10作者:蔡怀权

本文以 PowerToys 官方开发文档 doc/devdocs/processes/update-process.md 为主线,系统讲解 PowerToys 的自动更新体系:如何检测新版本、如何持久化更新状态、如何在 GPO 与用户设置约束下下载和安装更新。读完后,你将能够完整理解从"检查更新"按钮点击到 WiX 安装包静默替换的整条调用链,并掌握官方推荐的调试与故障排查手段。

一、机制总览与关键文件

PowerToys 的更新机制由"检测 + 状态持久化 + 独立更新器"三部分组成:Runner 进程周期性或手动触发版本检测,检测结果写入本地状态文件;当用户接受更新时,由随安装包分发的 PowerToys.Update.exe 完成下载、签名校验与静默安装。文档在 update-process.md 中给出的关键文件如下,本文按这些文件展开源码级分析:

文件 职责 仓库路径
updating.h / updating.cpp 版本检测、安装器下载、清理等核心更新逻辑 src/common/updating/updating.cpp
updateState.h / updateState.cpp 更新状态的读取与保存(含文件锁) src/common/updating/updateState.cpp
updateLifecycle.h 更新器两阶段的命令行参数构建与安全校验 src/common/updating/updateLifecycle.h
PowerToys.Update.cpp 独立更新器主程序(Stage 1 / Stage 2) src/Update/PowerToys.Update.cpp
UpdateUtils.cpp Runner 内的周期性更新工作线程与 Toast 通知 src/runner/UpdateUtils.cpp

二、版本检测:GitHub API 与安装器资产匹配

文档"Version Detection"一节指出:更新检测使用 GitHub API 获取最新版本信息,API 返回包含版本号与资产列表(assets)的 JSON;客户端按**架构(ARM64 或 X64)安装范围(user 或 machine)**匹配正确的安装器资产。源码证实了具体实现:

1. 两个 API 端点。updating.cpp 中定义了两个非本地化常量:

  • https://api.github.com/repos/microsoft/PowerToys/releases/latest —— 常规检查,获取最新稳定版;
  • https://api.github.com/repos/microsoft/PowerToys/releases?per_page=100 —— 仅当用户开启"包含预览版更新"时才使用,遍历前 100 条 release 记录,从中挑选高于当前版本的最大版本号(代码注释指出 GitHub 不保证 release 的返回顺序,因此不能提前 break)。

2. 本地构建跳过检查。 若版本主、次号均为 0(0.0.*,即构建农场的本地构建),get_github_version_info_async 直接返回 Local build cannot be updated,不会发起网络请求。

3. 资产匹配规则。 extract_installer_asset_download_info 的匹配逻辑是"扩展名 × 架构 × 文件名模式"三条件同时命中,且扩展名按优先级降序尝试 .exe 优先于 .msi。文件名模式常量定义在 updating.h

// non-localized
constexpr inline std::wstring_view INSTALLER_FILENAME_PATTERN = L"powertoyssetup";
constexpr inline std::wstring_view INSTALLER_FILENAME_PATTERN_USER = L"powertoysusersetup";

当前安装范围为 per-user 时使用 powertoysusersetup 模式,per-machine 时使用 powertoyssetup 模式——这与下文安装器工程的 GUID/MSI 命名一一对应。

4. 版本比较与结果类型。 检测结果用 std::variant<new_version_download_info, version_up_to_date> 表达:GitHub 版本号不高于当前版本时返回 version_up_to_date;否则返回包含 release 页面 URL、版本号、安装器下载 URL、安装器文件名、是否预览版的 new_version_download_info。任何异常(网络失败等)统一收敛为 Network error 字符串,避免调用方崩溃——PowerToys.Update.cpp 中特意加了对"错误值解引用"的防御注释,说明这是曾经出过问题的位置。

5. 下载重试。 download_new_version_async 通过 http::HttpClient 下载安装器到挂起更新目录,MAX_DOWNLOAD_ATTEMPTS 固定为 3 次(updating.cpp),全部失败才返回空值。

三、安装范围(Installation Scope)与升级码

文档指出 PowerToys 区分 user 安装器与 machine 安装器,两者有各自不同的文件名模式与升级码(upgrade code),且"这些码必须保持一致才能正确升级"。这一点在 WiX 安装器工程中可以得到直接印证。installer/PowerToysSetupVNext/Common.wxi 根据构建变量 PerUser 切换两套定义:

维度 Per-User Per-Machine
MSI 文件名 PowerToysUserSetup-<版本>-<平台>.msi PowerToysSetup-<版本>-<平台>.msi
默认安装目录 LocalAppDataFolder ProgramFiles64Folder
注册表作用域 HKCU HKLM
权限 limited(非提权) elevated(提权)
UpgradeCode D8B559DB-4C98-487A-A33F-50A8EEE42726 42B84BF7-5FBF-473B-9C8B-049DC16F7708

MSI 文件名中的 PowerToysUserSetup / PowerToysSetup 正是第二节日下载匹配所用的 powertoysusersetup / powertoyssetup 模式(不区分大小写);MSI 名中包含架构字符串,满足 architecture_matched 条件。WiX 安装器还会在注册表中记录 BundleUpgradeCode,供自定义操作判断同范围旧版本的存在(见 CustomAction.cpp 中的升级码比对逻辑),从而保证同作用域的新版本能作为升级覆盖安装,而不是并排共存。

四、更新状态文件:UpdateState 的结构与持久化

文档"Update State"一节列出了状态文件包含的信息:当前更新状态、release 页面 URL、上次检查时间、是否有新版本可用、安装器是否已下载。源码中对应 UpdateState 结构体

struct UpdateState
{
    enum State
    {
        upToDate = 0,
        errorDownloading = 1,
        readyToDownload = 2,
        readyToInstall = 3,
        networkError = 4
    } state = upToDate;
    std::wstring releasePageUrl;
    std::optional<std::time_t> githubUpdateLastCheckedDate;
    std::wstring downloadedInstallerFilename;
    bool isPrerelease = false;

    static void store(std::function<void(UpdateState&)> stateModifier);
    static UpdateState read();
};

各状态值的含义:upToDate(无新版本)、readyToDownload(发现新版本但尚未下载)、readyToInstall(安装器已下载完成,等待用户触发安装)、errorDownloading(下载失败)、networkError(获取版本信息失败)。

存储路径与命名。 文档给出的路径为 %LOCALAPPDATA%\Microsoft\PowerToys\update_state.json。当前代码中文件名常量是 UpdateState.jsonupdateState.cpp,拼在 Runner 的根保存目录下);而带下划线的旧文件名 update_state.json 仍保留在设置 UI 的向后兼容测试资产中,例如 V0.21.1 测试状态文件,可见项目专门用历史版本的状态文件回归验证过格式兼容性。

并发与迁移策略。 从源码结构看,有两个值得注意的工程细节:

  1. readstore 都会先获取命名互斥量 Local\PowerToysRunnerUpdateStateMutex,防止 Runner 与更新器并发改写状态文件;
  2. 序列化时会写入 updateStateFileVersion 字段。读取时若该字段版本与当前可执行文件版本不一致(IsOldFileVersion),则删除旧文件并写回默认状态——即状态文件随版本演进自动"重置",避免旧格式状态污染新版本逻辑。

状态迁移即更新流程本身。 结合 UpdateUtils.cppProcessNewVersionInfo 可以看出状态机如何流转:

  • 版本最新 → 置回 upToDate 并清空安装器文件名;
  • 允许自动下载且尚未下载 → 先执行 cleanup_updates() 清理旧安装器,然后下载,成功置 readyToInstall 并记录文件名,失败置 errorDownloading
  • 不允许自动下载 → 置 readyToDownload,仅提示用户去设置页手动触发。

cleanup_updates()updating.cpp)除了删除挂起更新目录中残留的 .exe/.msi,还会顺带清理根保存目录下不含当前版本号的旧 .log 日志文件。

五、更新检查:自动与手动两条触发路径

文档"Update Checking"区分了手动检查(设置页点击 "Check for Updates")与自动检查(周期性 update worker)。src/runner/UpdateUtils.cpp 给出了自动检查的关键常量:

constexpr int64_t UPDATE_CHECK_INTERVAL_MINUTES = 60 * 24;          // 常规检查间隔:24 小时
constexpr int64_t UPDATE_CHECK_AFTER_FAILED_INTERVAL_MINUTES = 60 * 2; // 失败后重试间隔:2 小时
const int UPDATE_NOTIFICATION_TOAST_SUSPEND_MINOR_VERSION_COUNT = 2; // Toast 挂起覆盖的次版本号数

两条路径的实现要点:

  • 自动检查PeriodicUpdateWorker()UpdateUtils.cpp)是 Runner 启动时的常驻循环,根据状态文件里的 githubUpdateLastCheckedDate 计算距离 24 小时检查点的剩余睡眠时间,醒来后拉取 GitHub 版本信息并处理结果;若本次获取失败,则缩短为 2 小时后重试。
  • 手动检查CheckForUpdatesCallback() 由设置页的"检查更新"动作触发,逻辑与自动检查相同,但传 show_notifications = false——手动检查不会主动弹 Toast(避免用户反复点击反复弹窗),仅在确有可用更新且状态变化时通过托盘图标提示。
  • 是否自动下载由三处条件共同决定:!IsMeteredConnection() && get_general_settings().downloadUpdatesAutomatically,再叠加 GPO disable automatic update download 的强制关闭(见第六节)。
  • 预览版开关effective_include_prerelease_updates() 会先查 DisablePreviewUpdates GPO,策略启用时无条件关闭预览更新检查,否则才读取用户设置 include_prerelease_updates

IsMeteredConnection()UpdateUtils.cpp)基于 WinRT Windows.Networking.Connectivity 判断:蜂窝(WWAN)连接,或漫游、超出流量上限、连接成本为 Fixed/Variable,均视为计费等效连接,从而抑制自动下载。

六、GPO 更新策略(组策略)

文档"User Settings / GPO Update Settings"一节列出了三个与更新相关的组策略,并在 doc/devdocs/processes/gpo.md 的 "Update-Related GPO Settings" 中再次确认:

组策略 作用
disable automatic update download 阻止自动下载安装器(仍可手动检查与手动下载)
disable new update toast 控制是否显示"有新版本"的 Toast 通知
suspend new update toast 在 2 个次版本(minor release)范围内挂起 Toast 通知

源码中三者分别对应 gpo.hgetDisableAutomaticUpdateDownloadValue / getSuspendNewUpdateToastValue / getDisableNewUpdateToastValue。挂起逻辑的实际算法在 ProcessNewVersionInfo:当挂起策略启用且新版本的 minor - 已安装 minor <= 2 时(且主版本号不大于当前)抑制通知;一旦落后超过 2 个次版本(例如 0.60.0 已装、0.63.* 发布)则恢复通知。代码注释特别提醒:修改挂起阈值时必须同步更新 ADML 文档,保证组策略描述与实际行为一致。

此外还有第四个更新相关策略 DisablePreviewUpdates:启用后强制关闭预览(prerelease)更新检查,稳定版更新不受影响(UpdateUtils.cpp)。

按照 gpo.md 的说明,策略值保存在注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PowerToys(机器级优先于用户级)或 HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\PowerToys(用户级)下,DWORD 值 1 表示启用、0 表示禁用、键值不存在表示未配置;本地测试时可用 regedit 直接创建这些值后重启 PowerToys 验证。

七、PowerToys.Update.exe:两阶段安装流程

文档"PowerToys Updater"小节描述了 PowerToysUpdate.exe 的职责:随安装分发、负责下载安装器并在用户点击更新通知时被调用。当前实现是 src/Update/PowerToys.Update.cpp(输出名 PowerToys.Update.exe),WinMain 依据第一个参数分派两个阶段(Stage 标志常量定义在其资源头文件中),整体流程如下:

Stage 1(普通权限,由更新器本体执行),见 WinMain

  1. 配置备份:先调用 updating::BackupConfigFilesconfigBackup.h)备份根保存目录下的配置文件,防止安装过程损坏用户数据;
  2. 获取安装器ObtainInstaller 读取 UpdateState——若状态已是 readyToInstall,则直接用 IsSafeDownloadedInstallerFilenameupdateLifecycle.h)严格校验缓存的文件名(拒绝任何路径分隔符、..、盘符与目录分量),再用 weakly_canonical 规范化后确认解析结果仍位于 Updates 目录内,随后免网络安装;否则根据是否包含预览版重新调用 get_github_version_info_async,对 readyToDownload / errorDownloading 状态先 cleanup_updates() 再重新下载;
  3. 关闭 PowerToys 并派生 Stage 2InstallNewVersionStage1PowerToys.Update.cpp)先把自身复制为 %TEMP%\PowerToys.Update.<PID>.exe(避免安装替换目录中正在运行的文件,并顺手清理上一次遗留的孤儿更新器副本),然后查找托盘图标窗口,先打开进程句柄再发送 WM_CLOSE(防止 PowerToys 在 WM_CLOSE 处理中退出后 PID 被复用),WaitForSingleObject 等待其真正退出,最后通过 ShellExecuteExWBuildStage2Arguments 构建的参数(Stage 2 标志 + 安装器路径 + 安装目录)启动临时的自身副本。

Stage 2(提权执行安装),见 InstallNewVersionStage2

  1. 签名信任校验:安装器存放在用户可写的 %LOCALAPPDATA%\Microsoft\PowerToys\Updates 目录,而 Stage 2 又是提权运行的。为防止本地非提权用户在"下载后、执行前"窗口期内替换安装器(TOCTOU 提权,代码注释标注为 MSRC 112000 缓解措施),Stage 2 以 FILE_SHARE_READ(拒绝写/删除共享)打开安装器文件,调用 updating::verify_installer_trust 验证其为 Microsoft Authenticode 签名,并保持句柄开放直到启动完成,确保"校验过的字节 == 执行的字节";
  2. 执行安装.msiMsiInstallProductW;其余视为 WiX 引导程序,以 /passive /norestart 参数启动并等待进程结束,退出码为 0 才算成功;
  3. 收尾:成功则把 UpdateState 重置为 upToDate 并刷新检查时间戳;之后无论成败都会执行 RestoreCorruptedConfigs 检查并修复可能损坏的配置文件;若提供了安装目录参数(CanRelaunchAfterUpdate 要求至少 4 个参数),则从安装目录重新启动 PowerToys.exe 并传入更新成功的报告参数。

八、更新通知(Toast)

文档"Update Notification"描述了通知闭环:新更新可用时 Toast 出现在 Windows 操作中心,点击后启动更新流程,更新器必要时下载安装器,安装器以相应命令行参数运行。ShowNewVersionAvailable 的实现补充了通知的细节:

  • 通知正文显示"当前版本 → 新版本",预览版会在版本号后附加 -preview 并使用独立的资源字符串;
  • 带两个操作按钮:Update now(深链 powertoys://update_now/,最终拉起 PowerToys.Update.exe 的 Stage 1)与 More info(深链 powertoys://open_overview/ 打开设置概览页);
  • 同一 UPDATING_PROCESS_TOAST_TAG 标签会先移除旧通知,保证屏幕上至多一条更新提示;
  • 自动下载模式下,若被 GPO/设置抑制了 Toast(见第六节),通知不弹,但托盘图标仍通过 set_tray_icon_update_available(true) 保持"有可用更新"提示。

九、版本编号与安装包细节

文档"Version Numbering / Installer Details"给出的规则:

  • 采用语义化版本 MAJOR.MINOR.PATCH;常规发布递增 MINOR(如 0.89.0),热修复递增 PATCH(如 0.87.0 → 0.87.1);
  • 安装包使用 WiX 引导程序(bootstrapper),并为 per-user 与 per-machine 分别定义升级码,升级码必须保持稳定才能正确完成升级覆盖。

从源码可以补充两点佐证:VersionHelpersrc/common/version/helper.h)统一负责 tag_name 的解析与比较,更新状态文件的 updateStateFileVersion 也用它做版本迁移判断(第四节);WiX 工程的 Common.wxi 中两个升级码以 <?define UpgradeCodeGUID=...> 形式硬编码,正是"必须保持一致"承诺的落点——修改任一 GUID 都会使该作用域的旧安装无法被识别为同一产品的升级。

十、调试技巧与常见问题

继承文档"Debugging Tips"一节,并对应到源码行为:

强制触发更新检查。 修改状态文件中的时间戳字段 githubUpdateLastCheckedDate 为一个更早的日期,然后退出 PowerToys、修改文件、再启动 PowerToys。原理是 PeriodicUpdateWorker 依据该字段计算距 24 小时检查点的剩余睡眠(第五节),时间戳越早、唤醒越早。注意该文件读写受 Local\PowerToysRunnerUpdateStateMutex 互斥量保护,编辑前请先退出 PowerToys。

常见故障(文档列举的四类及源码印证):

  • 权限问题导致无法下载:per-machine 安装器的更新器需要提权运行,Stage 2 提权失败或目标目录不可写会直接失败并写日志;
  • 网络中断下载:下载有 3 次重试,全部失败后状态置 errorDownloading,下次手动检查或 2 小时后重试时重新拉取;
  • 组策略阻止更新disable automatic update download 只禁自动下载、不禁检测;disable new update toast 只禁通知、更新仍会静默准备好。排查时优先查注册表策略值;
  • 应用运行中导致安装失败:Stage 1 已针对该场景设计了"关窗口 → 等进程退出 → 再启动安装器"的时序(第七节),若 PowerToys 拒绝退出(WaitForSingleObject 10 秒超时),文件锁仍可能导致安装器报错。

查看更新日志。 文档给出的日志路径为 %LOCALAPPDATA%\Microsoft\PowerToys\Logs\PowerToys-*.log,其中查找 update 相关的 trace/error 条目。另外更新器自身也单独初始化了一个以 LogSettings::updateLogPath 为路径的日志(PowerToys.Update.cpp),排查"点击更新后无反应"时应同时查看两份日志。关键日志锚点包括:Discovered new versionDownloading installer for a new versionAutomatic download of updates is disabled by GPOAborting update: downloaded installer failed trust verification

十一、发布与回滚考虑(Rollout Considerations)

文档"Rollout Considerations"明确了当前的发布策略:更新对所有用户同时可用,目前没有灰度(staged rollout)机制;发布后发现严重问题只能靠热修复(hotfix)解决,热修复的制作流程见 release-process.md。这一设计也解释了为何状态文件采用"按版本号重置"(第四节)、以及为何补丁版本只递增 PATCH 位——在无灰度的前提下,快速、可回退的小版本修复是唯一的止损手段。

十二、小结

PowerToys 的更新机制可以用一句话概括:Runner 负责"发现",状态文件负责"记住",独立的 PowerToys.Update.exe 负责"安装"。三个关注点解耦带来的直接好处是:更新器可以独立于 Runner 崩溃或退出而完成收尾,两阶段设计与 Authenticode 校验把"用户可写目录 + 提权执行"这一安全难点显式处理掉了。对于维护者或排障者而言,记住四个锚点即可定位绝大多数问题:GitHub API 端点(第二节的两个 URL)、状态文件 UpdateState.json 的五个状态值、WiX 的两个升级码,以及 Stage 1/Stage 2 的边界。

登录后查看全文
热门项目推荐
相关项目推荐