.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元件有一些不足,但相信經過本文的介紹,大家已經知道如何避坑來使用該元件。總體來說,該元件的功能還是蠻好用的。考慮到該元件只有作者一個人維護,其實已經做得蠻好了,還是要感謝作者的付出的。