Squirrel.Windows 自定义 Squirrel 事件完整指南:让应用响应安装、更新与卸载生命周期

原创2026-09-26 11:19:26343 阅读
文章标签:开发工具

Squirrel.Windows 自定义 Squirrel 事件完整指南:让应用响应安装、更新与卸载生命周期

Squirrel.Windows 的安装与更新框架在安装时默认不会执行任何"业务代码"——它只是把 NuGet 包内容解压到本地应用目录。本指南围绕 docs/using/custom-squirrel-events.md 展开,讲解如何通过标记"Squirrel-Aware"让安装在应用内的 EXE 在安装、更新、卸载等关键节点被调用,从而创建快捷方式、注册卸载项、弹出欢迎界面等。读完本文,你将掌握 C# 与原生(非 C#)应用的两种 Squirrel-Aware 标记方式、SquirrelAwareApp.HandleEvents 的正确用法、五个事件命令行参数的语义,以及底层调度机制的源码级实现。

为什么需要自定义 Squirrel 事件

Squirrel 的设计哲学是:框架本身在安装时几乎不做任何事情。它只负责把包解压、把 Update.exe 放到应用目录,然后调用你的应用去完成剩余的工作。文档原文明确指出:"Squirrel doesn't do much of anything at installation time automatically"(Squirrel 在安装时不会自动做太多事情)。

这与传统"installer DLL"方案形成鲜明对比:由于 Squirrel 的事件回调代码是运行在你自己应用进程内的,你可以直接使用自己项目的全部代码、类库和配置来完成安装/更新逻辑,而不是在一个受限的独立安装器进程里重新实现一遍。例如在 onInitialInstall 回调中,你可以直接调用 mgr.CreateShortcutForThisExe() 创建快捷方式,也可以顺便注册文件关联、写入自己的配置文件。

默认行为:Squirrel-Aware 之前的自动快捷方式

当你的包中没有任何 EXE 被标记为 Squirrel-Aware 时,Squirrel 会替你做一件事:为应用包中的每一个 EXE 自动在桌面(Desktop)和开始菜单(Start Menu)各创建一个快捷方式。

但一旦你为哪怕一个 EXE 启用了 Squirrel 事件,这个自动行为就会关闭——快捷方式的创建责任完全转移到你的代码里。这是最容易被忽略的"坑":标记 Squirrel-Aware 后如果不写创建快捷方式的回调,用户安装完会发现桌面和开始菜单里什么都没有。

从源码看,这个自动行为实现在 src/Squirrel/UpdateManager.ApplyReleases.cs 的 invokePostInstall 方法中:当检测到的 Squirrel-Aware 应用数量为 0 时,框架会遍历版本目录下所有非 squirrel. 前缀的 EXE,并为它们调用 CreateShortcutsForExecutable(..., ShortcutLocation.Desktop | ShortcutLocation.StartMenu, ...)。

第一步:让应用变得 Squirrel Aware

要让 Squirrel 在安装/更新/卸载时调用你的 EXE,必须先在 EXE 的元数据里声明 SquirrelAwareVersion = 1。Squirrel 通过 src/Squirrel/SquirrelAwareExecutableDetector.cs 中的 GetPESquirrelAwareVersion 检测该值,检测顺序为:先查 .NET 程序集的自定义属性,查不到再查 PE 版本资源块(Version Block)。

C# 应用:通过 AssemblyInfo.cs 声明

在你的 AssemblyInfo.cs 中添加一行:

[assembly: AssemblyMetadata("SquirrelAwareVersion", "1")]

这是 C#(.NET Framework / .NET Core / .NET 5+)应用最直接的方式。底层检测逻辑在 SquirrelAwareExecutableDetector.GetAssemblySquirrelAwareVersion(src/Squirrel/SquirrelAwareExecutableDetector.cs):它用 Mono.Cecil 读取程序集自定义属性,找到 System.Reflection.AssemblyMetadataAttribute 且键名为 SquirrelAwareVersion 的属性,再把值解析为整数;解析失败则视为未标记。

非 C# 应用:通过 Version Block 声明

对于 C++、Rust、Go 等非托管应用,需要把 SquirrelAwareVersion 写进 PE 的英文版本信息块(English Version Block),通常通过 App.rc 资源文件完成。典型的条目如下:

BLOCK "StringFileInfo"
BEGIN
    BLOCK "040904b0"
    BEGIN
        VALUE "FileDescription", "Installer for Squirrel-based applications"
        VALUE "FileVersion", "0.5.0.0"
        VALUE "InternalName", "Setup.exe"
        VALUE "LegalCopyright", "Copyright (C) 2014"
        VALUE "OriginalFilename", "Setup.exe"
        VALUE "ProductName", "Squirrel-based application"
        VALUE "ProductVersion", "0.5.0.0"
        VALUE "SquirrelAwareVersion", "1"
    END
END

底层检测逻辑在 GetVersionBlockSquirrelAwareValue(src/Squirrel/SquirrelAwareExecutableDetector.cs):

  • 通过 GetFileVersionInfoSize / GetFileVersionInfo / VerQueryValue 查询版本资源;
  • 只认两种语言代码:040904B0(英语-美国)和 000004B0(语言中性),源码中以常量 englishUS = "040904B0"、neutral = "000004B0" 硬编码;
  • 只要在版本块中找到 SquirrelAwareVersion 名称就直接返回 1——源码注释里作者坦诚说明:由于 Atom.exe 存在版本号解析异常,这里选择"只要找到名字就算 Squirrel-Aware"的保守策略。

注意:Windows 商店风格的 MSIX 打包或改名为 .appx 的 EXE 不会被此路径检测到,检测基于经典 PE 文件。

第二步:用 SquirrelAwareApp 帮助类处理事件(C# 推荐)

对于 C# 应用,文档强烈推荐使用 SquirrelAwareApp 帮助类(src/Squirrel/SquirrelAwareApp.cs)来实现事件处理。这是一个静态类,核心方法 HandleEvents 接收五个可选回调:

参数 触发时机 回调签名 回调返回后应用是否退出
onInitialInstall 初始安装完成 Action<Version> 退出(exit 0)
onAppUpdate 应用更新到新版本 Action<Version> 退出(exit 0)
onAppObsoleted 应用不再是新版本(用户装了更新的版本) Action<Version> 退出(exit 0)
onAppUninstall 通过"程序和功能"卸载 Action<Version> 退出(exit 0)
onFirstRun 安装后首次正常运行 Action 不退出,正常进入主流程

文档给出的标准实现(该实现复刻了默认的非 Squirrel-Aware 行为):

static bool ShowTheWelcomeWizard;
...
static int Main(string[] args)
{
    // NB: Note here that HandleEvents is being called as early in startup
    // as possible in the app. This is very important! Do _not_ call this
    // method as part of your app's "check for updates" code.

    using (var mgr = new UpdateManager(updateUrl))
    {
        // Note, in most of these scenarios, the app exits after this method
        // completes!
        SquirrelAwareApp.HandleEvents(
          onInitialInstall: v => mgr.CreateShortcutForThisExe(),
          onAppUpdate: v => mgr.CreateShortcutForThisExe(),
          onAppUninstall: v => mgr.RemoveShortcutForThisExe(),
          onFirstRun: () => ShowTheWelcomeWizard = true);
    }
}

要点一:必须在启动最早期调用。文档用醒目注释提醒:不要把 HandleEvents 放在"检查更新"的代码路径里。原因在于安装/更新/卸载这些事件是通过命令行参数注入的——如果应用先走完自身的启动逻辑(加载配置、连接服务、弹窗)再处理事件,会造成启动缓慢、与正常业务流程耦合、甚至错过退出时机。

要点二:大多数事件回调结束后应用必须退出。源码中 HandleEvents 在分发完事件后调用 Environment.Exit(0)(src/Squirrel/SquirrelAwareApp.cs),因为 Squirrel 安装流程需要你的进程尽快让出控制权、继续后续步骤;回调抛出异常则记录错误日志并 Environment.Exit(-1)。唯一例外是 onFirstRun——它直接 return,应用继续正常启动。

要点三:HandleEvents 的 arguments 参数默认取 Environment.GetCommandLineArgs(),仅在做单元测试时通过该参数 mock 命令行;生产代码保持 null 即可(src/Squirrel/SquirrelAwareApp.cs)。

第三步(非 C#):直接处理启动命令行参数

非 C# 应用无法使用 SquirrelAwareApp,必须在 main()/入口函数里自行解析命令行。Squirrel 会以如下特殊参数启动你的 EXE,文档要求你正确区分并处理:

参数 语义 处理建议
--squirrel-install x.y.z.m 应用被安装时调用 完成应用设置(如创建快捷方式、注册表项)后尽快退出
--squirrel-firstrun 所有安装步骤完成之后调用 当作一次正常启动处理(可展示"欢迎"界面),正常继续运行
--squirrel-updated x.y.z.m 应用更新到指定版本时调用 完成后尽快退出
--squirrel-obsolete x.y.z.m 你的旧版本不再是新版本时调用 清理旧版本残留后尽快退出
--squirrel-uninstall x.y.z.m 应用被卸载时调用 清理自己创建的东西(快捷方式、注册表项)后尽快退出

其中 x.y.z.m 是四段式版本号。调用方是 Update.exe(安装/更新时),卸载时同样由 Update.exe --uninstall 触发。

事件调度与参数注入的源码证据

这些参数并非约定俗成,而是由 src/Squirrel/UpdateManager.ApplyReleases.cs 的 invokePostInstall 方法真实构造并注入的:

  • 安装时构造 --squirrel-install {currentVersion},更新时构造 --squirrel-updated {currentVersion}(第 413-415 行);
  • 对目录中所有 Squirrel-Aware 的 EXE 逐一顺序执行(并发度 1),每个进程带 15 秒超时(cts.CancelAfter(15 * 1000)),某个 hook 失败只记录日志、不中断安装(第 422-432 行);
  • 若是初始安装且非静默安装,最后会对每个 Squirrel-Aware EXE 以 --squirrel-firstrun 参数 Process.Start 启动且不等待(第 450-455 行);
  • 旧版本被淘汰时,会以 --squirrel-obsolete {version} 调用对应版本目录下的应用(src/Squirrel/UpdateManager.ApplyReleases.cs);
  • 卸载流程会以 --squirrel-uninstall {version} 调用(src/Squirrel/UpdateManager.ApplyReleases.cs)。

从源码结构可以推断:每个事件参数后面跟的版本号,是 Squirrel 当前正在处理的版本目录(app-x.y.z.m)对应的语义版本。C# 侧 SquirrelAwareApp.HandleEvents 收到的 Version 即解析后的该值。

App Setup Helper Methods:事件回调里的实用工具

以下方法帮助你在 Squirrel 事件回调中完成应用设置。文档特别说明:如果没在使用自定义 Squirrel 事件,通常不需要调用这些方法(因为框架会自动处理快捷方式)。

快捷方式:CreateShortcutsForExecutable / RemoveShortcutsForExecutable

ShortcutLocation 是带 <a href="https://link.gitcode.com/i/c991f06dbb8a8d67c9370956a84660f5" target="_blank">Flags] 的枚举([src/Squirrel/IUpdateManager.cs),可组合使用:

[Flags]
public enum ShortcutLocation {
    StartMenu = 1 << 0,   // 开始菜单
    Desktop  = 1 << 1,    // 桌面
    Startup  = 1 << 2,    // 启动文件夹(开机自启)
    AppRoot  = 1 << 3     // 应用目录内(适合便携应用)
}

实现细节值得注意:

  • 更新模式下不重建用户主动删除的快捷方式。源码判断 if (!fileExists && updateOnly) 就跳过——如果用户已手动删除快捷方式,Squirrel 认为这是用户意愿,更新时不会"骚扰"用户重新创建(第 233-236 行);
  • 快捷方式附带 AppUserModelID(com.squirrel.{packageId}.{exeName})和由该 ID 哈希生成的 Toast Activator CLSID(第 257-261 行),保证开始菜单磁贴分组与通知激活正确;
  • programArguments 参数会以 -a "..." 形式追加到快捷方式命令行;
  • 创建/删除后会调用 fixPinnedExecutables 同步修复任务栏已固定快捷方式的目标路径(指向新版本目录)。

对于最常见的"为当前正在运行的 EXE 创建/删除快捷方式",框架在 src/Squirrel/IUpdateManager.cs 提供了扩展方法 CreateShortcutForThisExe() / RemoveShortcutForThisExe(),它们内部自动定位入口程序集,并对 .NET Core 场景做了 DLL→EXE 的路径修正。这正是文档示例里 mgr.CreateShortcutForThisExe() 的实际调用。

卸载注册表项:CreateUninstallerRegistryEntry / RemoveUninstallerRegistryEntry

  • CreateUninstallerRegistryEntry():基于当前已应用的包,在"程序和功能"(HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\{应用名})创建卸载入口。实现见 src/Squirrel/UpdateManager.InstallHelpers.cs。它默认使用内置的 Update.exe --uninstall(静默开关 -s)作为卸载命令,并写入 DisplayName、DisplayVersion、Publisher、UninstallString、QuietUninstallString、EstimatedSize、NoModify、NoRepair 等键值;若包元数据带图标 URL,还会下载并转成 ICO 写入 DisplayIcon。
  • RemoveUninstallerRegistryEntry():删除上述注册表卸载项(src/Squirrel/UpdateManager.InstallHelpers.cs)。

文档指出这些方法通常由 Update.exe 调用(即框架内部已集成),开发者手动调用多用于自定义卸载入口或卸载清理场景。在标准更新流程 UpdateApp 中,框架会自动创建卸载注册表项(src/Squirrel/IUpdateManager.cs)。

典型完整实现:安装、更新、卸载与首启

将上述内容组合,一个覆盖全部五个事件的完整 C# 应用入口如下:

using System;
using Squirrel;

static class Program
{
    [STAThread]
    static int Main(string[] args)
    {
        // 必须在启动最早期调用 HandleEvents
        using (var mgr = new UpdateManager(@"https://example.com/releases"))
        {
            SquirrelAwareApp.HandleEvents(
                onInitialInstall: v =>
                {
                    mgr.CreateShortcutForThisExe();          // 桌面 + 开始菜单快捷方式
                    mgr.CreateUninstallerRegistryEntry();     // 程序和功能卸载入口
                    // 可在此注册文件关联、写入初始配置
                },
                onAppUpdate: v =>
                {
                    mgr.CreateShortcutForThisExe();           // 更新后修复快捷方式
                },
                onAppObsoleted: v =>
                {
                    // 旧版本被替换时的清理(通常很少需要做事)
                },
                onAppUninstall: v =>
                {
                    mgr.RemoveShortcutForThisExe();           // 删除快捷方式
                    mgr.RemoveUninstallerRegistryEntry();     // 删除卸载注册表项
                },
                onFirstRun: () =>
                {
                    // 不退出,继续正常运行,例如置位欢迎向导标记
                    ShowTheWelcomeWizard = true;
                });
        }

        // 正常应用启动逻辑(仅当未走任何 Squirrel 事件分支时到达此处)
        if (ShowTheWelcomeWizard)
            ShowWelcomeWizard();

        RunMainWindow();
        return 0;
    }
}

测试与验证:Squirrel-Aware 检测

仓库的测试项目 test/Squirrel.Tests/SquirrelAwareExecutableDetectorTests.cs 提供了检测逻辑的完整验证矩阵,可直接参考或复用:

  • AtomShellShouldBeSquirrelAware:用 fixtures/atom.exe(Version Block 方式标记)断言 GetPESquirrelAwareVersion == 1;
  • SquirrelAwareViaVersionBlock / SquirrelAwareViaLanguageNeutralVersionBlock:分别验证 040904B0 与 000004B0 两种语言代码的版本块都能被识别(后者用 fixtures/SquirrelAwareTweakedNetCoreApp.exe);
  • SquirrelAwareViaAssemblyAttribute:验证 AssemblyMetadata("SquirrelAwareVersion", "1") 属性方式;
  • NotSquirrelAware / NotSquirrelAwareTestAppShouldNotBeSquirrelAware:验证未标记的 EXE(如 Update.exe、fixtures/NotSquirrelAwareApp.exe)返回 null。

编写完成后,你可以用 SquirrelAwareExecutableDetector.GetPESquirrelAwareVersion 快速自检产物 EXE 是否被正确标记——这是排查"为什么我的应用没收到 Squirrel 事件"的首选手段。

常见问题排查

  1. 标记后没有快捷方式:确认是否在 onInitialInstall / onAppUpdate 中调用了 CreateShortcutForThisExe(或等效的 CreateShortcutsForExecutable)。标记 Squirrel-Aware 后自动快捷方式已关闭。
  2. 事件回调没被触发:检查 EXE 是否真的带 SquirrelAwareVersion = 1(C# 用属性、原生用 Version Block,语言代码必须是 040904B0 或 000004B0);确认目录下的 EXE 文件名没有 squirrel. 前缀且是标准 PE。
  3. 回调后应用没退出:除 onFirstRun 外的四个回调返回后框架会强制 Environment.Exit(0);若你的代码在此之前启动了后台线程或打开了窗口,应主动尽快返回。
  4. 卸载不干净:在 onAppUninstall 中务必对称清理 onInitialInstall 创建的所有东西(快捷方式、注册表项、文件关联)。

延伸阅读

登录后查看全文
Squirrel.Windows