net 客戶端程式自動更新
當我們在日常開發中編寫的客戶端程式需要部署在多台主機上時,如果程式需要升級,那麼一台台升級會非常麻煩,此時就可以使用本文的.net 客戶端程式自動更新技術。
本文所述的自动更新技术主要使用了开源的GeneralUpdate组件,可用于Winform/WPF/ConsoleApp等应用程序的自动更新。
GeneralUpdate组件是微软的一位 MVP 负责开发和维护的,仓库地址及截图如下。作者提供的使用文档和视频有些过于简单,而且不同版本还存在一定的兼容性问题,这些都没有很好地解释,所以初次接触这个组件的开发人员可能会有点懵。笔者结合自己在项目中实际的使用情况,更加详细地介绍一下该组件的使用方式。
GitHub:https://github.com/WELL-E/AutoUpdater

Gitee:https://gitee.com/Juster-zhu/GeneralUpdate

自動更新流程圖

鑑於原圖的說明不夠明確,筆者在上圖中使用紅色字體新增了說明。上圖中看上去是 3 個組件或服務的交互,但準確說是 4 個:
- 客戶端程式版本校驗服務(非必須):該服務至少提供兩個 api,一個是用於判斷客戶端程式有沒有最新版本,另一個是獲取當前客戶端的所有更新版本。有些時候我們並不想單獨編寫並部署一個校驗服務,那麼我們就可以直接用資料庫來替代。客戶端程式直接查詢資料庫,判斷並獲取當前程式的所有更新版本。
- 客戶端程式(必須):需要具有自動更新功能的業務程式,可以通過反射獲取自身程式集的版本號,並和服務端/資料庫比對,判斷是否有新版本。
- 更新組件(必須):更新組件實際上是一個單獨的可執行文件,放在和客戶端程式的同級目錄下。該組件的主要作用是從指定路徑下下載客戶端程式的所有更新壓縮包,並逐個解壓,實現客戶端程式的逐版本升級。當客戶端從服務端獲取到待更新文件的路徑時,需要通過進程間通信啟動更新組件,更新組件啟動後需要關閉客戶端程式以防止某些文件被占用導致更新失敗。更新組件更新成功後重新啟動客戶端,並關閉組件自身,完成自動更新。
- 文件伺服器(必須):客戶端程式的更新壓縮包上傳到文件伺服器後得到每個壓縮包的 url,更新組件根據該 url 下載程式。筆者用的文件伺服器是 hfs,下載地址為:hfs 下載。
代碼結構剖析

上图中以GeneralUpdate开头的工程是自动更新功能的核心代码,在 nuget 服务器上能看到各个工程的包。具体使用哪个包取决于你是想实现更新组件自更新还是更新客户端程序还是编写版本校验服务,可参考框架 README.md 中的介绍。
这里要说明的是,上述组件不是向下兼容的!3.x.x 版本的组件的很多方法都进行了更名,因此不能直接从 2.x.x 版本直接升级。
上图中以AutoUpdate开头的工程是对自动更新流程图中 3 个主要组件的简单实现:
ConsoleApp:更新组件的控制台版本 DEMO(需要和文件服务器配合使用,引入了 GeneralUpdate.Core)MauiApp-Sample:未仔细研究,不清楚MinimalService:客户端版本校验服务 DEMO(引入了 GeneralUpdate.AspNetCore)Test:更新组件自更新的 WPF 版本 DEMO(需要和 MinimalService 配合使用,引入了 GeneralUpdate.ClientCore)WpfApp:GeneralUpdate.Single包的使用 DEMO,用于构建单例版本的更新组件(引入了 GeneralUpdate.Single)WpfNet6-Sample:更新更新组件的 WPF 版本程序。
winform 應用程式的自動更新實戰
从上节的描述可知,如果我们不想编写客户端版本校验服务,只想通过文件服务器来更新客户端程序,那么我们只需要一个控制台版本的更新组件即可,所以可参考ConsoleApp工程下的代码。
更新組件的控制台實現
说明:本示例使用的是GeneralUpdate.Core的 2.1.6 版本。因为 GitHub 上的源码已升级到 3.x.x 版本,支持了.NET 6.0,但笔者电脑上的缺乏相关框架,无法编译通过,所以检出到了源码的某次提交,这样即使使用的时候出了问题也可以通过调试源码的方式来解决。如果大家充分理解了本文的意思,直接安装最新版本的 nuget 包也可以,直接参考最新版源码的相关示例。
using System;
using System.ComponentModel;
using System.Diagnostics;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Strategys;
using GeneralUpdate.Core.Update;
using ProgressChangedEventArgs = GeneralUpdate.Core.Update.ProgressChangedEventArgs;
namespace AutoUpdate.ConsoleApp
{
class Program
{
static void Main(string[] args)
{
// args = new []{
// "1.0.1",
// "1.0.2",
// "",
// "http://127.0.0.1:7000/client_v1.0.2.zip",
// @"D:\Project",
// "36aad55a19f85ee6e1fbdc26510a26c1"
// };
KillProcess("你的客户端程序名,不用加exe");
GeneralUpdateBootstrap bootstrap = new GeneralUpdateBootstrap();
bootstrap.DownloadStatistics += OnDownloadStatistics;
bootstrap.ProgressChanged += OnProgressChanged;
bootstrap.Strategy<DefultStrategy>().
Option(UpdateOption.Format, "zip").
Option(UpdateOption.MainApp, "你的客户端程序名,不用加exe").
Option(UpdateOption.DownloadTimeOut, 60).
RemoteAddress(args).
Launch();
Console.ReadKey();
}
private static void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.Type == ProgressType.Updatefile)
{
var str = $"当前更新第:{e.ProgressValue}个,更新文件总数:{e.TotalSize}";
Console.WriteLine(str);
}
if (e.Type == ProgressType.Done)
{
Console.WriteLine("更新完成");
}
if (e.Type == ProgressType.Fail)
{
Console.WriteLine(e.Message);
}
}
private static void OnDownloadStatistics(object sender, DownloadStatisticsEventArgs e)
{
Console.WriteLine($"下载速度:{e.Speed},剩余时间:{e.Remaining.Minute}:{e.Remaining.Second}");
}
private static void KillProcess(string processName)
{
foreach (var process in Process.GetProcesses())
{
if (!process.ProcessName.ToUpper().Contains(processName.ToUpper())) continue;
try
{
process.Kill();
process.WaitForExit();
}
catch (Win32Exception)
{
}
}
}
}
}
客戶端調用
Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
var ver = $"{version.Major}.{version.Minor}.{version.Build}";
//从数据库获取比当前程序集版本更高的版本信息
var versionInfo = TOSBll.Instance.GetLastUpdateVersionInfo(1, ver);
if (versionInfo != null)
{
string para =
$"{ver} {versionInfo.VERSION} \"\" {versionInfo.URL} {Environment.CurrentDirectory} {versionInfo.MD5}";
ExecuteAsAdmin("AutoUpdate.ConsoleApp.exe", para);
return;
}
private static void ExecuteAsAdmin(string fileName, string args)
{
Process proc = new Process();
proc.StartInfo.FileName = fileName;
proc.StartInfo.UseShellExecute = true;
proc.StartInfo.Verb = "runas";
proc.StartInfo.Arguments = args;
proc.Start();
}
由上述代码可知,客户端使用进程间通信的方式来启动更新组件,并传入更新参数信息。这里通过管理员权限启动更新组件,以免更新失败(组件在更新时需要把文件拷贝到系统的临时目录,更新成功后删除,权限不足时会出错)。不过笔者测试中发现这种方式启动仍然失败,还是通过右键AutoUpdate.ConsoleApp.exe程序并附加管理员权限才成功的。
幾個槽點
- 關鍵版本不打標籤,使用者想切換到 nuget 包的 2.1.6 版本都不知道該檢出到哪次提交。
- 新版本組件不兼容老版本。
- 單元測試文件中使用的代碼是老版本的,組件源碼卻是新版本的,直接把剛接觸該組件的人員給弄懵圈了。
- 目前还存在一些小 bug,比如
FileUtil.Update32Or64Libs()就会抛出异常,因为把一个目录删除了两遍,从而导致第一次启动更新的时候更新失败,但是第二次更新的时候却能成功,因为目前已经删了。笔者已提 Issue,不知作者何时能解决。 - 文檔過於簡單。
總結
虽然GeneralUpdate组件有一些不足,但相信经过本文的介绍,大家已经知道如何避坑来使用该组件。总体来说,该组件的功能还是蛮好用的。考虑到该组件只有作者一个人维护,其实已经做得蛮好了,还是要感谢作者的付出的。