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


    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
    et45 目录作为 app 所在文件夹-->
        <file src="bin**" target="lib
    et45" />
      </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上面有篇文章详细介绍了这几种自动更新方案的实现细节, 详情见文末链接.

  • 相关阅读:
    什么是Netflix Feign?它的优点是什么?
    Spring Boot 自动配置原理是什么?
    springcloud断路器作用?
    什么是SpringCloudConfig?
    find命令查找包含指定内容的文件
    @PostConstruct使用总结
    @Retention 注解的作用
    SpringBoot自定义Condition注解
    Spring Boot 入门
    SpringBoot +MSSQL
  • 原文地址:https://www.cnblogs.com/Kane-Blake/p/14787827.html
Copyright © 2020-2023  润新知