當前位置: 妍妍網 > 碼農

.NET 程式自動更新元件

2024-06-07碼農

前言

本來博主想偷懶使用AutoUpdater.NET元件,但由於博主計畫有些特殊性和它的功能過於多,於是博主自己實作一個輕量級獨立自動更新元件,可稍作修改整合到大家自己計畫中,比如:WPF/Winform/Windows服務。

大致思路:發現更新後,從網路上下載更新包並進行解壓,同時在 WinForms 應用程式中顯示下載和解壓進度條,並重新開機程式。以提供更好的使用者體驗。

系統架構概覽

自動化軟體更新系統主要包括以下幾個核心部份:

  • 版本檢查 :定期或在啟動時檢查伺服器上的最新版本。

  • 下載更新 :如果發現新版本,則從伺服器下載更新包。

  • 解壓縮與安裝 :解壓下載的更新包,替換舊檔。

  • 重新開機套用 :更新完畢後,重新開機套用以載入新版本。

  • 元件實作細節

    獨立更新程式邏輯

    1、建立 WinForms 應用程式

    首先,建立一個新的 WinForms 應用程式,用來承載獨立的自動更新程式,界面就簡單兩個元件:添加一個 ProgressBar 和一個 TextBox 控制項,用於顯示進度和資訊提示。

    2、主表單載入事件

    我們在主表單的 Load 事件中完成以下步驟:

  • 解析命令列參數。

  • 關閉當前執行的程式。

  • 下載更新包並顯示下載進度。

  • 解壓更新包並顯示解壓進度。

  • 啟動解壓後的新版本程式。

  • 下面是主表單 Form1_Load 事件處理常式的程式碼:

    privateasyncvoidForm1_Load(object sender, EventArgs e)
    {
    // 讀取和解析命令列參數
    var args = Environment.GetCommandLineArgs();
    if (!ParseArguments(args, outstring downloadUrl, outstring programToLaunch, outstring 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、解析命令列參數

    我們需要從命令列接收下載地址、啟動程式名稱和當前運行程式的名稱。以下是解析命令列參數的程式碼:

    privateboolParseArguments(string[] args, outstring downloadUrl, outstring programToLaunch, outstring 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);
    }
    elseif (args[i] == "--url" && i + 1 < args.Length)
    {
    downloadUrl = args[++i];
    }
    elseif (args[i].StartsWith("--launch="))
    {
    programToLaunch = args[i].Substring("--launch=".Length);
    }
    elseif (args[i] == "--launch" && i + 1 < args.Length)
    {
    programToLaunch = args[++i];
    }
    elseif (args[i].StartsWith("--current="))
    {
    currentProgram = args[i].Substring("--current=".Length);
    }
    elseif (args[i] == "--current" && i + 1 < args.Length)
    {
    currentProgram = args[++i];
    }
    }
    return !string.IsNullOrEmpty(downloadUrl) && !string.IsNullOrEmpty(programToLaunch) && !string.IsNullOrEmpty(currentProgram);
    }

    4、下載更新包並顯示進度

    使用 HttpClient 下載檔,並在下載過程中更新進度條:

    privateasync 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, 8192true))
    {
    var buffer = newbyte[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 檔(因為當前更新程式正在執行,大家可根據需求修改邏輯),並捕獲異常以確保進度條和界面更新:

    privatevoidExtractZipFile(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、啟動解壓後的新程式

    在解壓完成後,啟動新版本的程式,並且關閉更新程式:

    privatevoidForm1_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 類,對外提供參照,用於檢查更新並啟動更新程式:

    publicstatic classUpdateChecker
    {
    publicstaticstring UpdateUrl { getset; }
    publicstaticstring CurrentVersion { getset; }
    publicstaticstring MainProgramRelativePath { getset; }
    publicstaticvoidCheckForUpdates()
    {
    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>

    主程式呼叫更新檢查

    主程式可以透過定時器或者手動呼叫檢查更新的邏輯,博主使用定時檢查更新:

    internalstatic classAutoUpdaterHelp
    {
    privatestaticreadonly System.Timers.Timer timer;
    staticAutoUpdaterHelp()
    {
    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();
    };
    }
    publicstaticvoidStart()
    {
    timer.Start();
    }
    publicstaticvoidStop()
    {
    timer.Stop();
    }
    }

    思考:效能與安全考量

    在實作自動化更新時,還應考慮效能和安全因素。例如,為了提高效率,可以添加斷點續傳功能;為了保證安全,應驗證下載檔的完整性,例如使用SHA256校驗和,這些博主就不做實作與講解了,目前的功能已經完成了基本的自動更新邏輯

    總結

    自動化軟體更新是現代軟體開發不可或缺的一部份,它不僅能顯著提升使用者體驗,還能減輕開發者的維護負擔。透過上述C#程式碼範例,你可以快速搭建一個基本的自動化更新框架,進一步完善和客製以適應特定的套用場景。

    本文提供了構建自動化軟體更新系統的C#程式碼實作,希望對開發者們有所幫助。如果你有任何疑問或建議,歡迎留言討論!

    轉自:極客Bob

    連結:cnblogs.com/Bob-luo/p/18231510

    - EOF -

    推薦閱讀 點選標題可跳轉

    看完本文有收獲?請轉發分享給更多人

    推薦關註「DotNet」,提升.Net技能

    點贊和在看就是最大的支持❤️