前言 本來博主想偷懶使用AutoUpdater.NET組件,但由于博主項(xiàng)目有些特殊性和它的功能過于多,于是博主自己實(shí)現(xiàn)一個(gè)輕量級獨(dú)立自動(dòng)更新組件,可稍作修改集成到大家自己項(xiàng)目中,比如:WPF/Winform/Windows服務(wù)。
大致思路: 發(fā)現(xiàn)更新后,從網(wǎng)絡(luò)上下載更新包并進(jìn)行解壓,同時(shí)在 WinForms 應(yīng)用程序中顯示下載和解壓進(jìn)度條,并重啟程序。以提供更好的用戶體驗(yàn)。
系統(tǒng)架構(gòu)概覽
自動(dòng)化軟件更新系統(tǒng)主要包括以下幾個(gè)核心部分:
版本檢查 :定期或在啟動(dòng)時(shí)檢查服務(wù)器上的最新版本。
下載更新 :如果發(fā)現(xiàn)新版本,則從服務(wù)器下載更新包。
重啟應(yīng)用 :更新完畢后,重啟應(yīng)用以加載新版本。
組件實(shí)現(xiàn)細(xì)節(jié) 獨(dú)立更新程序邏輯 1、創(chuàng)建 WinForms 應(yīng)用程序
首先,創(chuàng)建一個(gè)新的 WinForms 應(yīng)用程序,用來承載獨(dú)立的自動(dòng)更新程序,界面就簡單兩個(gè)組件:
添加一個(gè) ProgressBar
和一個(gè) TextBox
控件,用于顯示進(jìn)度和信息提示。
2、主窗體加載事件
我們在主窗體的 Load
事件中完成以下步驟:
關(guān)閉當(dāng)前運(yùn)行的程序。
下面是主窗體 Form1_Load
事件處理程序的代碼:
private async void Form1_Load (object sender, EventArgs e ) { // 讀取和解析命令行參數(shù) var args = Environment.GetCommandLineArgs(); if (!ParseArguments(args, out string downloadUrl, out string programToLaunch, out string currentProgram)) { _ = MessageBox.Show("請?zhí)峁┯行У南螺d地址和啟動(dòng)程序名稱的參數(shù)。" ); Application.Exit(); return ; } // 關(guān)閉當(dāng)前運(yùn)行的程序 Process[] processes = Process.GetProcessesByName(currentProgram); foreach (Process process in processes) { process.Kill(); process.WaitForExit(); } // 開始下載和解壓過程 string downloadPath = Path.Combine(Path.GetTempPath(), Path.GetFileName(downloadUrl)); progressBar.Value = 0 ; textBoxInformation.Text = "下載中..." ; await DownloadFileAsync(downloadUrl, downloadPath); progressBar.Value = 0 ; textBoxInformation.Text = "解壓中..." ; await Task.Run(() => ExtractZipFile(downloadPath, AppDomain.CurrentDomain.BaseDirectory)); textBoxInformation.Text = "完成" ; // 啟動(dòng)解壓后的程序 string programPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, programToLaunch); if (File.Exists(programPath)) { _ = Process.Start(programPath); Application.Exit(); } else { _ = MessageBox.Show($"無法找到程序:{programPath} " ); } }
3、解析命令行參數(shù)
我們需要從命令行接收下載地址、啟動(dòng)程序名稱和當(dāng)前運(yùn)行程序的名稱。以下是解析命令行參數(shù)的代碼:
private bool ParseArguments (string [] args, out string downloadUrl, out string programToLaunch, out string currentProgram ) { downloadUrl = null ; programToLaunch = null ; currentProgram = null ; for (int i = 1 ; i < args.Length; i++) { if (args[i].StartsWith("--url=" )) { downloadUrl = args[i].Substring("--url=" .Length); } else if (args[i] == "--url" && i + 1 < args.Length) { downloadUrl = args[++i]; } else if (args[i].StartsWith("--launch=" )) { programToLaunch = args[i].Substring("--launch=" .Length); } else if (args[i] == "--launch" && i + 1 < args.Length) { programToLaunch = args[++i]; } else if (args[i].StartsWith("--current=" )) { currentProgram = args[i].Substring("--current=" .Length); } else if (args[i] == "--current" && i + 1 < args.Length) { currentProgram = args[++i]; } } return !string .IsNullOrEmpty(downloadUrl) && !string .IsNullOrEmpty(programToLaunch) && !string .IsNullOrEmpty(currentProgram); }
4、下載更新包并顯示進(jìn)度
使用 HttpClient
下載文件,并在下載過程中更新進(jìn)度條:
private async Task DownloadFileAsync (string url, string destinationPath ) { using (HttpClient client = new HttpClient()) { using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) { _ = response.EnsureSuccessStatusCode(); long ? totalBytes = response.Content.Headers.ContentLength; using (var stream = await response.Content.ReadAsStreamAsync()) using (var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192 , true )) { var buffer = new byte [8192 ]; long totalRead = 0 ; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer, 0 , buffer.Length)) != 0 ) { await fileStream.WriteAsync(buffer, 0 , bytesRead); totalRead += bytesRead; if (totalBytes.HasValue) { int progress = (int )((double )totalRead / totalBytes.Value * 100 ); _ = Invoke(new Action(() => progressBar.Value = progress)); } } } } } }
5、解壓更新包并顯示進(jìn)度
在解壓過程中跳過 Updater.exe
文件(因?yàn)楫?dāng)前更新程序正在運(yùn)行,大家可根據(jù)需求修改邏輯),并捕獲異常以確保進(jìn)度條和界面更新:
private void ExtractZipFile (string zipFilePath, string extractPath ) { using (ZipArchive archive = ZipFile.OpenRead(zipFilePath)) { int totalEntries = archive.Entries.Count; int extractedEntries = 0 ; foreach (ZipArchiveEntry entry in archive.Entries) { try { // 跳過 Updater.exe 文件 if (entry.FullName.Equals(CustConst.AppNmae, StringComparison.OrdinalIgnoreCase)) { continue ; } string destinationPath = Path.Combine(extractPath, entry.FullName); _ = Invoke(new Action(() => textBoxInformation.Text = $"解壓中... {entry.FullName} " )); if (string .IsNullOrEmpty(entry.Name)) { // Create directory _ = Directory.CreateDirectory(destinationPath); } else { // Ensure directory exists _ = Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); // Extract file entry.ExtractToFile(destinationPath, overwrite: true ); } extractedEntries++; int progress = (int )((double )extractedEntries / totalEntries * 100 ); _ = Invoke(new Action(() => progressBar.Value = progress)); } catch (Exception ex) { _ = Invoke(new Action(() => textBoxInformation.Text = $"解壓失?。?span style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; color: rgb(224, 108, 117); line-height: 26px;">{entry.FullName} , 錯(cuò)誤: {ex.Message} ")); continue ; } } } }
6、啟動(dòng)解壓后的新程序
在解壓完成后,啟動(dòng)新版本的程序,并且關(guān)閉更新程序:
private void Form1_Load (object sender, EventArgs e ) { // 省略部分代碼... string programPath = Path.Combine(extractPath, programToLaunch); if (File.Exists(programPath)) { Process.Start(programPath); Application.Exit(); } else { MessageBox.Show($"無法找到程序:{programPath} " ); } }
檢查更新邏輯 1、創(chuàng)建 UpdateChecker
類
創(chuàng)建一個(gè) UpdateChecker
類,對外提供引用,用于檢查更新并啟動(dòng)更新程序:
public static class UpdateChecker { public static string UpdateUrl { get ; set ; } public static string CurrentVersion { get ; set ; } public static string MainProgramRelativePath { get ; set ; } public static void CheckForUpdates () { try { using (HttpClient client = new HttpClient()) { string xmlContent = client.GetStringAsync(UpdateUrl).Result; XDocument xmlDoc = XDocument.Parse(xmlContent); var latestVersion = xmlDoc.Root.Element("version" )?.Value; var downloadUrl = xmlDoc.Root.Element("url" )?.Value; if (!string .IsNullOrEmpty(latestVersion) && !string .IsNullOrEmpty(downloadUrl) && latestVersion != CurrentVersion) { // 獲取當(dāng)前程序名稱 string currentProcessName = Process.GetCurrentProcess().ProcessName; // 啟動(dòng)更新程序并傳遞當(dāng)前程序名稱 string arguments = $"--url \"{downloadUrl} \" --launch \"{MainProgramRelativePath} \" --current \"{currentProcessName} \"" ; _ = Process.Start(CustConst.AppNmae, arguments); // 關(guān)閉當(dāng)前主程序 Application.Exit(); } } } catch (Exception ex) { _ = MessageBox.Show($"檢查更新失敗:{ex.Message} " ); } } }
2、服務(wù)器配置XML
服務(wù)器上存放一個(gè)XML文件配置當(dāng)前最新版本、安裝包下載地址等,假設(shè)服務(wù)器上的 XML 文件內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?> <update > <version > 1.0.2</version > <url > https://example.com/yourfile.zip</url > </update >
主程序調(diào)用更新檢查 主程序可以通過定時(shí)器或者手動(dòng)調(diào)用檢查更新的邏輯,博主使用定時(shí)檢查更新:
internal static class AutoUpdaterHelp { private static readonly System.Timers.Timer timer; static AutoUpdaterHelp () { UpdateChecker.CurrentVersion = "1.0.1" ; UpdateChecker.UpdateUrl = ConfigurationManager.AppSettings["AutoUpdaterUrl" ].ToString(); UpdateChecker.MainProgramRelativePath = "Restart.bat" ; timer = new System.Timers.Timer { Interval = 10 * 1000 //2 * 60 * 1000 }; timer.Elapsed += delegate { UpdateChecker.CheckForUpdates(); }; } public static void Start () { timer.Start(); } public static void Stop () { timer.Stop(); } }
思考:性能與安全考量 在實(shí)現(xiàn)自動(dòng)化更新時(shí),還應(yīng)考慮性能和安全因素。例如,為了提高效率,可以添加斷點(diǎn)續(xù)傳功能;為了保證安全,應(yīng)驗(yàn)證下載文件的完整性,例如使用SHA256校驗(yàn)和,這些博主就不做實(shí)現(xiàn)與講解了,目前的功能已經(jīng)完成了基本的自動(dòng)更新邏輯
總結(jié)
自動(dòng)化軟件更新是現(xiàn)代軟件開發(fā)不可或缺的一部分,它不僅能顯著提升用戶體驗(yàn),還能減輕開發(fā)者的維護(hù)負(fù)擔(dān)。
通過上述C#代碼示例,你可以快速搭建一個(gè)基本的自動(dòng)化更新框架,進(jìn)一步完善和定制以適應(yīng)特定的應(yīng)用場景。
閱讀原文:原文鏈接 ?
該文章在 2025/5/6 15:21:10 編輯過