Squirrel.Windows 自定义 Squirrel 事件完整指南:让应用响应安装、更新与卸载生命周期
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
CreateShortcutsForExecutable(string exeName, ShortcutLocation locations, bool updateOnly, string programArguments, string icon):在桌面或开始菜单为指定 EXE 创建快捷方式。定义于 src/Squirrel/IUpdateManager.cs,实现于 src/Squirrel/UpdateManager.ApplyReleases.cs。RemoveShortcutsForExecutable(string exeName, ShortcutLocation locations):删除对应快捷方式(src/Squirrel/UpdateManager.ApplyReleases.cs)。
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 事件"的首选手段。
常见问题排查
- 标记后没有快捷方式:确认是否在
onInitialInstall/onAppUpdate中调用了CreateShortcutForThisExe(或等效的CreateShortcutsForExecutable)。标记 Squirrel-Aware 后自动快捷方式已关闭。 - 事件回调没被触发:检查 EXE 是否真的带
SquirrelAwareVersion = 1(C# 用属性、原生用 Version Block,语言代码必须是040904B0或000004B0);确认目录下的 EXE 文件名没有squirrel.前缀且是标准 PE。 - 回调后应用没退出:除
onFirstRun外的四个回调返回后框架会强制Environment.Exit(0);若你的代码在此之前启动了后台线程或打开了窗口,应主动尽快返回。 - 卸载不干净:在
onAppUninstall中务必对称清理onInitialInstall创建的所有东西(快捷方式、注册表项、文件关联)。
延伸阅读
- Custom Squirrel Events for non-C# Apps(非 C# 应用的自定义 Squirrel 事件) — 原生应用的 Squirrel-Aware 标记与命令行参数处理详解;
- Update Manager(更新管理器) —
UpdateManager的完整 API 与更新流程; - Install Process(安装过程) — 安装时
Update.exe与应用的完整交互时序; - Update Process(更新过程) — 更新时事件触发顺序;
- 应用签名(Application Signing) — 修改 Version Block 资源时需要注意的签名问题。