Windows 程序自动更新方案: Squirrel.Windows

1. Squirrel

Squirrel 是一组工具和适用于.Net的库,用于管理 Desktop Windows 应用程序的安装和更新. Squirrel 对 Desktop Windows 应用程序的实现语言没有任何要求.

2. 下载相关工具

3. 环境准备

  • 解压下载的 Squirrel.Windows.zip 文件.
  • 因为 Squirrel.Windows 中自带的 rcedit.exe 是比较老的版本, 不支持中文字符集,所以需要下载最新的 rcedit.exe 文件覆盖到 Squirrel.Windows 目录中.
  • 将 nuget.exe 、rcedit.exe 及 Squirrel.exe 所在文件夹加入到环境变量 Path 中,方便命令行使用
  • Squirrel.exe 通过 调用 NativeMethods.VerQueryValue 方法在可执行文件的版本资源(BLOCK "040904B0") 中查找 SquirrelAwareVersion 信息. 若存在该值且大于等于1, 则认为该程序为 SquirrelAwareApp. 但是 dotnet5 可执行文件的 VersionInfo 存储在 BLOCK "000004b0" 中, 所以 Squirrel 的 SquirrelAware 功能暂时不支持 dotnet5 应用.
  • 有人已经提交了修复该问题的 PR , 截止到现在该 PR 尚未合并. 为解决 dotnet5 程序里面 SquirrelAwareVersion 的问题, 需要自己通过修改源码重新发布 Squirrel.exe 的方式来增加 dotnet5 的支持. 另外一种更简单的方法是通过 DnSpy 反编译修改 Squirrel.SquirrelAwareExecutableDetector.GetVersionBlockSquirrelAwareValue() 方法, 修改后如下:
int fileVersionInfoSize = NativeMethods.GetFileVersionInfoSize(executable, IntPtr.Zero);
if (fileVersionInfoSize <= 0 || fileVersionInfoSize > 4096)
{
return null;
}
byte[] array = new byte[fileVersionInfoSize];
if (!NativeMethods.GetFileVersionInfo(executable, 0, fileVersionInfoSize, array))
{
return null;
}
IntPtr intPtr;
int num;
if (!new string[]
{
"040904B0",
"000004B0"
}.Any((string languageCode) => NativeMethods.VerQueryValue(array, "\\StringFileInfo\\" + languageCode + "\\SquirrelAwareVersion", out intPtr, out num)))
{
return null;
}
return new int?(1);

4. 使用 Squirrel 发布更新包及安装包

  • 准备好需要集成自动更新的程序.

创建一个 bin 文件夹,将可执行 exe 文件及所有依赖文件拷贝进去.

  • 可选: 使用 rcedit 设置可执行文件的 version string.

设置完成后可以通过 Resource Hacker 查看是否正确设置.(如果设置该项, 需要自己处理 Squirrel 事件来创建桌面快捷方式. 如果不设置,会自动查找所有的 .exe 文件,并建立快捷方式)

rcedit ./bin/MyApp.exe --set-version-string "SquirrelAwareVersion" "1"
  • 创建并修改 .nuspec 文件.

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<!--包名、应用安装位置名称-->
<id>MyApp</id>
<version>2.0.0</version>
<!--快捷方式名称、windows应用管理器中的应用名称-->
<title>包装确认台</title>
<authors>SanHua Inc.</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>XXX包装确认台.</description>
<!--指定 codepage 支持中文字符-->
<language>zh-CN</language>
</metadata>
<files>
<!--Squirrel.Windows默认使用 lib\net45\ 目录作为 app 所在文件夹-->
<file src="bin\**" target="lib\net45\" />
</files>
</package>
  • 使用 nuget 命令进行打包

nuget pack MyApp.nuspec
  • 使用 Squirrel 命令发布更新包.

可以通过 -icon -setupIcon 选项来指定图标

Squirrel --releasify MyApp.2.0.0.nupkg -icon favicon.ico -setupIcon favicon.ico
  • Releases 目录

Squirrel --releasify命令成功执行后会生成一个 Releases 文件夹. 该文件夹下的内容需要保留, 用于下次发布时生成 *-delta.nupkg 增量更新包.

- RELEASES                  # 该文件记录了各个版本包的名称、hash、大小
- Setup.exe # 提供给用户的 exe 安装包
- Setup.msi # 提供给用户的 msi 安装包
- MyApp-2.0.0-full.nupkg # 基础包
- MyApp-2.0.1-full.nupkg # 全量更新包
- MyApp-2.0.1-delta.nupkg # 增量更新包
  • app 安装目录

用户安装 app 后, 安装位置为 %LocalAppData%\MyApp, 目录结构如下:

- RELEASES          # 该文件记录了各个版本包的名称、hash、大小
- SquirrelSetup.log # Squirrel 运行日志
- Update.exe # Squirrel.exe 的拷贝, 用于执行 Squirrel 命令来实现应用的更新、卸载、创建快捷方式等等
- MyApp.exe # StubExecutable.exe 的拷贝, 该 c++ 程序查找 ./app-x.y.z 目录下与自己同名的 exe
- # 通过启动新进程来调用最新版的 app.exe , 比如 app-2.0.1/MyApp.exe
- # 创建的桌面快捷方式指向的就是它
- app-2.0.0/** # 上个版本 app 安装位置
- app-2.0.1/** # 最新版本 app 安装位置

5. 使用 Squirrel 进行自动更新

  • 将 RELEASES 、*-full.nupkg、 *-delta.nupkg 文件托管到静态文件服务器上.

  • 通过调用Update.exe --UpdateUrl remoteUrl来实现自动更新.

  • Squirrel 事件

Squirrel 进程通过启动子进程调用你的应用来传递事件消息. 比如说第一次安装完成时, 会通过以下命令来传递事件消息:

MyApp.exe --squirrel-install x.y.z.m

  • 使用 C# 代码实现后台定时检查更新

以下代码使用 Process 启动更新程序, 并在程序中使用定时器来定期检查更新.

需要注意的是执行安装程序时, 会先将程序解压到 %LocalAppData%\SquirrelTemp 目录中, 此时 squirrel.exe 的工作目录也在此处. 在安装结束启动新进程调用 app 传递事件消息时, 子进程的工作目录默认与父进程相同. 所以在处理--squirrel-install事件时, 为了调用 squirrel.exe 来创建快捷方式, 必须指定绝对路径, 否则会找不到可执行文件.

public static async Task<int> InvokeProcessAsync(string fileName, string arguments, string workingDirectory = null)
{
var activity = new Activity(SquirrelDiagnosticListenerExtensions.ExecuteCommand);
s_diagnosticListener.WriteStartActivity(activity, new { fileName, arguments, workingDirectory, AppDomain.CurrentDomain.BaseDirectory }); using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo()
{
// 可执行文件查找顺序:
// 1. 绝对路径
// 2. Environment.ProcessPath 或 Process.GetCurrentProcess().MainModule.FileName 下查找
// 3. Directory.GetCurrentDirectory() 下查找
// 4. PATH 环境变量中查找
FileName = fileName ?? string.Empty,
Arguments = arguments ?? string.Empty,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory ?? string.Empty,// 为空时取 Directory.GetCurrentDirectory()
RedirectStandardInput = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
process.Start(); var cts = new CancellationTokenSource();
cts.CancelAfter(1000 * 300);
await process.WaitForExitAsync(cts.Token); // 设置进程超时时间 bool Timeout = false;
if (!process.HasExited)
{
Timeout = true;
process.Kill(); // 如果超时, 则强制退出进程
} var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
s_diagnosticListener.WriteStopActivity(activity, new { process.ExitCode, Timeout, stdout, stderr }); return process.ExitCode;
}
} public void EanbleAutoUpdate()
{
if (_timer == null)
{
var timer = new System.Threading.Timer(async (object state) => await UpdateAsync(), null, Timeout.Infinite, Timeout.Infinite);
Interlocked.CompareExchange(ref _timer, timer, null);
}
_timer.Change(TimeSpan.FromSeconds(10), TimeSpan.FromMinutes(3));
}
  • 在程序入口点处理 squirrel 事件

以下代码在初次安装成功后创建桌面快捷方式, 在卸载时删除快捷方式.

var AppDirectory = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".."));
var SquirrelFilePath = Path.Combine(AppDirectory, "Update.exe");
var AppName = new DirectoryInfo(AppDirectory).Name; await UpdateManager.HandleSquirrelEventsAsync(
async version => await UpdateManager.InvokeProcessAsync(SquirrelFilePath, $"--createShortcut {AppName}.exe", AppDirectory),//创建快捷方式
async version => await UpdateManager.InvokeProcessAsync(SquirrelFilePath, $"--removeShortcut {AppName}.exe", AppDirectory),//删除快捷方式
(version) => { MessageBox.Show($"onAppUpdate 版本:{version} 已下载完成, 请关闭应用."); return Task.CompletedTask; },
(version) => { MessageBox.Show("onAppObsoleted"); return Task.CompletedTask; },
() => { MessageBox.Show("欢迎使用本 APP !"); return Task.CompletedTask; });
  • HandleSquirrelEventsAsync 方法

Squirrel.SquirrelAwareApp.HandleEvents 方法用于帮助处理 Squirrel 事件. 以下代码在源码的基础上调整为异步委托, 并在委托调用失败时, 将错误信息写入 Windows Application event log, 方便调试.

/// <summary>
/// 处理 Squirrel 事件
/// </summary>
/// <param name="onInitialInstall">在应用程序初始化安装结束时调用</param>
/// <param name="onAppUpdate">在应用程序更新结束时调用</param>
/// <param name="onAppObsoleted">在应用程序不是最新版本时调用</param>
/// <param name="onAppUninstall">在应用程序卸载结束时调用</param>
/// <param name="onFirstRun">在应用程序第一次启动时调用</param>
public static async Task HandleSquirrelEventsAsync(
Func<Version, Task> onInitialInstall = null,
Func<Version, Task> onAppUninstall = null,
Func<Version, Task> onAppUpdate = null,
Func<Version, Task> onAppObsoleted = null,
Func<Task> onFirstRun = null,
string[] arguments = null)
{
Func<Version, Task> defaultBlock = v => Task.CompletedTask;
var args = arguments ?? Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length == 0) return; var lookup = new[] {
new { Key = "--squirrel-install", Value = onInitialInstall ?? defaultBlock },
new { Key = "--squirrel-updated", Value = onAppUpdate ?? defaultBlock },
new { Key = "--squirrel-obsolete", Value = onAppObsoleted ?? defaultBlock },
new { Key = "--squirrel-uninstall", Value = onAppUninstall ?? defaultBlock },
}.ToDictionary(k => k.Key, v => v.Value); if (args[0] == "--squirrel-firstrun")
{
await onFirstRun?.Invoke();
return;
} if (args.Length != 2 || !lookup.ContainsKey(args[0]) )
{
return;
} try
{
var version = new Version(args[1]);
await lookup[args[0]](version);
Environment.Exit(0);
}
catch (Exception ex)
{
Environment.FailFast($"Fatal Exception Occurs When Handle Squirrel Events With Arguments '{args}'", ex);
}
}

6. 其他更新方案

除了 Squirrel 之外, 桌面平台的自动更新方案还有 Google OmahaAutoUpdater.NETWinSparkle等等. omaha-consulting.com上面有篇文章详细介绍了这几种自动更新方案的实现细节, 详情见文末链接.

Windows 程序自动更新方案: Squirrel.Windows的更多相关文章

  1. Windows 8.1 & Windows 10 取消 Windows Update 自动更新硬件驱动

    最新文章:Virson's Blog 1.打开控制面板,在搜索框中搜索“设备”一次,检索出相关的设备设置功能,如下图: 2.在检索出的结果中点击“更改设备安装设置”,会弹出设备驱动的更新方式,按照如下 ...

  2. 实现Windows程序的更新

    实现Windows程序的更新 一.使用枚举避免不合理的赋值 1.使用枚举的好处: 使用常量类中Student类中加入一个特别属性,StudentGender,而且这个属性只能接受两个有效值," ...

  3. .Net桌面程序自动更新NAppUpdate

    自动更新介绍 我们做了程序,不免会有版本升级,这就需要程序有自动版本升级的功能.应用程序自动更新是由客户端应用程序自身负责从一个已知服务器下载并安装更新,用户唯一需要进行干预的是决定是否愿意现在或以后 ...

  4. nw.js桌面程序自动更新(node.js表白记)

    Hello Google Node.js 一个基于Google V8 的JavaScript引擎. 一个伟大的端至端语言,或许我对你的热爱源自于web这门极富情感的技术吧! 注: 光阴似水,人生若梦, ...

  5. EF-使用迁移技术让程序自动更新数据库表结构

    承接上一篇文章:关于类库中EntityFramework之CodeFirst(代码优先)的操作浅析 本篇讲述的是怎么使用迁移技术让程序自动通过ORM框架将模型实体类结构映射到现有数据库,并新增或修改与 ...

  6. .net程序客户端更新方案

    原文:.net程序客户端更新方案 客户端程序一个很大的不便的地方就是程序集更新,本文这里简单的介绍一种通用的客户端更新方案.这个方案依赖程序集的动态加载,具体方案如下: 将程序集存储在一个文件数据库中 ...

  7. 禁用windows 10自动更新

    按Win键+R键调出运行,输入“gpedit.msc”点击“确定”,调出“本地组策略编辑器”.顺序依次展开计算机配置,管理模板 ,windows组件 ,windows更新 点击右边“配置自动更新”,选 ...

  8. 一种让运行在CentOS下的.NET CORE的Web项目简单方便易部署的自动更新方案

    一.项目运行环境 项目采用的是.NET5开发的Web系统,独立部署在省内异地多台CentOS服务器上,它们运行在甲方专网环境中(不接触互联网),甲方进行业务运作时(一段时间内)会要求异地服务器开机上线 ...

  9. 利用pre平台实现iOS应用程序自动更新

    // // AppDelegate.m // PreAutoUpdateDemo // // Created by mac on 15/12/18. // Copyright © 2015年 mac. ...

随机推荐

  1. P1164_小A点菜(JAVA语言)

    思路 简单动态规划问题 题目背景 uim神犇拿到了uoi的ra(镭牌)后,立刻拉着基友小A到了一家--餐馆,很低端的那种. uim指着墙上的价目表(太低级了没有菜单),说:"随便点" ...

  2. 习题3_08循环小数(JAVA语言)

    package 第三章习题; import java.util.Arrays; import java.util.Scanner; /*  * 输入整数a和b(0<=a<=3000,1&l ...

  3. PTA 中序输出叶子结点

    6-8 中序输出叶子结点 (10 分)   本题要求实现一个函数,按照中序遍历的顺序输出给定二叉树的叶结点. 函数接口定义: void InorderPrintLeaves( BiTree T); T ...

  4. juc下Condition类解析

    在使用Lock之前,我们使用的最多的同步方式应该是synchronized关键字来实现同步方式了.配合Object的wait().notify()系列方法可以实现等待/通知模式. Condition接 ...

  5. pwn题命令行解题脚本

    目录 脚本说明 脚本内容 使用 使用示例 参考与引用 脚本说明 这是专门为本地调试与远程答题准备的脚本,依靠命令行参数进行控制. 本脚本支持的功能有: 本地调试 开启tmux调试 设置gdb断点,支持 ...

  6. 仿VUE创建响应式数据

    VUE对于前端开发人员都非常熟悉了,其工作原理估计也都能说的清个大概,具体代码的实现估计看的人不会太多,这里对vue响应式数据做个简单的实现. 先简单介绍一下VUE数据响应原理,VUE响应数据分为对象 ...

  7. centos7.4 卸载python2.7.5安装python3.6.3版本

    CentOS 中默认安装了 2.7的Python,为了使用新版 python,可以对旧版本进行升级.但是由于很多基本的命令.软件包都依赖旧版本,比如:yum等.所以,在更新 Python 时,建议不要 ...

  8. 自动化kolla-ansible部署centos7.9+openstack-train-超融合高可用架构

    自动化kolla-ansible部署centos7.9+openstack-train-超融合高可用架构 欢迎加QQ群:1026880196 进行交流学习 环境说明: 1. 满足一台电脑一个网卡的环境 ...

  9. irreader网页订阅

    flag:立刻阅读,订阅你的全世界 订阅网页.RSS和Podcast,具备急速的阅读体验,高品质.免费.无广告.多平台的阅读器.泛用型Podcast播放器. 下载位置:http://irreader. ...

  10. 1037 Magic Coupon

    The magic shop in Mars is offering some magic coupons. Each coupon has an integer N printed on it, m ...