引言 本來博主想偷懶使用AutoUpdater.NET組件,但由於博主項目有些特殊性和它的功能過於多,於是博主自己實現一個輕量級獨立自動更新組件,可稍作修改集成到大家自己項目中,比如:WPF/Winform/Windows服務。大致思路:發現更新後,從網路上下載更新包併進行解壓,同時在 WinFor ...
引言
本來博主想偷懶使用AutoUpdater.NET組件,但由於博主項目有些特殊性和它的功能過於多,於是博主自己實現一個輕量級獨立自動更新組件,可稍作修改集成到大家自己項目中,比如:WPF/Winform/Windows服務。大致思路:發現更新後,從網路上下載更新包併進行解壓,同時在 WinForms 應用程式中顯示下載和解壓進度條,並重啟程式。以提供更好的用戶體驗。
1. 系統架構概覽
自動化軟體更新系統主要包括以下幾個核心部分:
- 版本檢查:定期或在啟動時檢查伺服器上的最新版本。
- 下載更新:如果發現新版本,則從伺服器下載更新包。
- 解壓縮與安裝:解壓下載的更新包,替換舊文件。
- 重啟應用:更新完畢後,重啟應用以載入新版本。
組件實現細節
獨立更新程式邏輯:
1. 創建 WinForms 應用程式
首先,創建一個新的 WinForms 應用程式,用來承載獨立的自動更新程式,界面就簡單兩個組件:添加一個 ProgressBar
和一個 TextBox
控制項,用於顯示進度和信息提示。
2. 主窗體載入事件
我們在主窗體的 Load
事件中完成以下步驟:
- 解析命令行參數。
- 關閉當前運行的程式。
- 下載更新包並顯示下載進度。
- 解壓更新包並顯示解壓進度。
- 啟動解壓後的新版本程式。
下麵是主窗體 Form1_Load
事件處理程式的代碼:
private async void Form1_Load(object sender, EventArgs e)
{
// 讀取和解析命令行參數
var args = Environment.GetCommandLineArgs();
if (!ParseArguments(args, out string downloadUrl, out string programToLaunch, out string currentProgram))
{
_ = MessageBox.Show("請提供有效的下載地址和啟動程式名稱的參數。");
Application.Exit();
return;
}
// 關閉當前運行的程式
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 = "完成";
// 啟動解壓後的程式
string programPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, programToLaunch);
if (File.Exists(programPath))
{
_ = Process.Start(programPath);
Application.Exit();
}
else
{
_ = MessageBox.Show($"無法找到程式:{programPath}");
}
}
3. 解析命令行參數
我們需要從命令行接收下載地址、啟動程式名稱和當前運行程式的名稱。以下是解析命令行參數的代碼:
查看代碼
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. 下載更新包並顯示進度
使用 HttpClient
下載文件,併在下載過程中更新進度條:
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. 解壓更新包並顯示進度
在解壓過程中跳過 Updater.exe
文件(因為當前更新程式正在運行,大家可根據需求修改邏輯),並捕獲異常以確保進度條和界面更新:
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 = $"解壓失敗:{entry.FullName}, 錯誤: {ex.Message}"));
continue;
}
}
}
}
6. 啟動解壓後的新程式
在解壓完成後,啟動新版本的程式,並且關閉更新程式:
查看代碼
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. 創建 UpdateChecker
類
創建一個 UpdateChecker
類,對外提供引用,用於檢查更新並啟動更新程式:
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)
{
// 獲取當前程式名稱
string currentProcessName = Process.GetCurrentProcess().ProcessName;
// 啟動更新程式並傳遞當前程式名稱
string arguments = $"--url \"{downloadUrl}\" --launch \"{MainProgramRelativePath}\" --current \"{currentProcessName}\"";
_ = Process.Start(CustConst.AppNmae, arguments);
// 關閉當前主程式
Application.Exit();
}
}
}
catch (Exception ex)
{
_ = MessageBox.Show($"檢查更新失敗:{ex.Message}");
}
}
}
2. 伺服器配置XML
伺服器上存放一個XML文件配置當前最新版本、安裝包下載地址等,假設伺服器上的 XML 文件內容如下:
<?xml version="1.0" encoding="utf-8"?>
<update>
<version>1.0.2</version>
<url>https://example.com/yourfile.zip</url>
</update>
主程式調用更新檢查
主程式可以通過定時器或者手動調用檢查更新的邏輯,博主使用定時檢查更新:
查看代碼
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();
}
}
思考:性能與安全考量
在實現自動化更新時,還應考慮性能和安全因素。例如,為了提高效率,可以添加斷點續傳功能;為了保證安全,應驗證下載文件的完整性,例如使用SHA256校驗和,這些博主就不做實現與講解了,目前的功能已經完成了基本的自動更新邏輯
結論
自動化軟體更新是現代軟體開發不可或缺的一部分,它不僅能顯著提升用戶體驗,還能減輕開發者的維護負擔。通過上述C#代碼示例,你可以快速搭建一個基本的自動化更新框架,進一步完善和定製以適應特定的應用場景。
本文提供了構建自動化軟體更新系統的C#代碼實現,希望對開發者們有所幫助。如果你有任何疑問或建議,歡迎留言討論!