• .NET C# 程序自动更新组件


    引言

    本来博主想偷懒使用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 文件内容如下:

    
    <update>
        <version>1.0.2version>
        <url>https://example.com/yourfile.zipurl>
    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#代码实现,希望对开发者们有所帮助。如果你有任何疑问或建议,欢迎留言讨论!


     

  • 相关阅读:
    【云原生之Docker实战】使用docker部署PicUploader图床工具
    【JAVA】给线程的interrupt()方法使用举个栗子
    常用的文本对比工具或网站
    【Android学习】自定义文本框和输入监听
    二进制安装k8s高可用部署
    应对气候、经济双重影响,此‘链路’非彼链路
    Simulink求解器综合介绍
    Java8 Stream源码精讲(四):一文说透四种终止操作
    .netCore用DispatchProxy实现动态代理
    网页游戏的开发框架
  • 原文地址:https://www.cnblogs.com/Bob-luo/p/18231510