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

net · 浏览次数 : 38

小编点评

引言 博主本来想使用AutoUpdater.NET组件进行自动更新,但由于项目特殊性和功能繁多,于是决定自己实现一个轻量级的独立自动更新组件。本文将介绍实现思路、系统架构概览、组件实现细节以及主程序调用相关内容。 1. 系统架构概览 自动化软件更新系统主要包括以下几个核心部分:版本检查、下载更新、解压缩与安装、重启应用。 2. 组件实现细节 独立更新程序逻辑包括:创建WinForms应用程序、解析命令行参数、下载更新包并显示进度、解压更新包并显示进度、启动解压后的新版本程序。 3. 解析命令行参数 编写代码解析命令行参数,如下载地址、启动程序名称和当前运行程序名称。 4. 下载更新包并显示进度 使用HttpClient下载文件,在下载过程中更新进度条。 5. 解压更新包并显示进度 解压过程中跳过Updater.exe文件,捕获异常以确保进度条和界面更新。 6. 启动解压后的新程序 在解压完成后,启动新版本的程序,并关闭更新程序。 7. 检查更新逻辑 创建Update Checker类,对外提供引用,用于检查更新并启动更新程序。 8. 服务器配置 在服务器上存放一个XML文件,配置当前最新版本、安装包下载地址等信息。 思考:性能与安全考量 在实现自动化更新时,还应考虑性能和安全因素,如添加断点续传功能和验证下载文件完整性等。 结论 自动化软件更新是现代软件开发不可或缺的一部分,能显著提升用户体验并减轻开发者维护负担。通过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 文件内容如下:

<?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#代码实现,希望对开发者们有所帮助。如果你有任何疑问或建议,欢迎留言讨论!


 

与.NET C# 程序自动更新组件相似的内容:

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

引言 本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务。大致思路:发现更新后,从网络上下载更新包并进行解压,同时在 WinFor

记一次 .NET 某自动化采集软件 崩溃分析

一:背景 1.讲故事 前段时间有位朋友找到我,说他的程序在客户的机器上跑着跑着会出现偶发卡死,然后就崩掉了,但在本地怎么也没复现,dump也抓到了,让我帮忙看下到底怎么回事,其实崩溃类的dump也有简单的,也有非常复杂的,因为大多情况下都是非托管层面出现的各种故障,非常考验对 C, C++, Win

.NET程序的 GDI句柄泄露 的再反思

## 一:背景 ### 1. 讲故事 上个月我写过一篇 [如何洞察 C# 程序的 GDI 句柄泄露](https://www.cnblogs.com/huangxincheng/p/17474733.html) 文章,当时用的是 GDIView + WinDbg 把问题搞定,前者用来定位泄露资源,后

C# 程序集、模块和类型概念及关系

目录C# 程序集、模块和类型概念及关系概述程序集模块类型程序集、模块和类型的关系总结引用 C# 程序集、模块和类型概念及关系 概述 在 C# 中,程序集、模块和类型是构成 .NET 应用程序的基本单元。它们之间具有以下层次关系: 程序集 是包含 .NET 代码的单元,它可以是一个可执行文件(EXE)

如何洞察 .NET程序 非托管句柄泄露

## 一:背景 ### 1. 讲故事 很多朋友可能会有疑问,C# 是一门托管语言,怎么可能会有非托管句柄泄露呢? 其实一旦 C# 程序与 C++ 语言交互之后,往往就会被后者拖入非托管泥潭,让我们这些调试者被迫探究 `非托管领域问题`。 ## 二:非托管句柄泄露 ### 1. 测试案例 为了方便讲述

[转帖]程序运行崩溃(segfault)的排查方法

这篇博文记录的非常详细:https://blog.csdn.net/zhaohaijie600/article/details/45246569 我的笔记: 写的C++程序老是运行两三天就挂了,关键是挂的时候连“segment fault”都不显示。动用了gdb、valgrind还是没办法,最后还是

像go 一样 打造.NET 单文件应用程序的编译器项目bflat 发布 7.0版本

现代.NET和C#在低级/系统程序以及与C/C++/Rust等互操作方面的能力完全令各位刮目相看了,有人用C#开发的64位操作系统: GitHub - nifanfa/MOOS: C# x64 operating system pro...,截图要介绍的是一个结合Roslyn和NativeAOT的实

C#.NET与JAVA互通之DES加密V2024

C#.NET与JAVA互通之DES加密V2024 配置视频: 环境: .NET Framework 4.6 控制台程序 JAVA这边:JDK8 (1.8) 控制台程序 注意点: 1.由于密钥、明文、密文的输入输出参数,都是byte数组(byte[]),所以:字符串转byte数组(byte[])环节,

使用.NET7和C#11打造最快的序列化程序-以MemoryPack为例

## 译者注 本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通过本文解释了他是如何将序列化程序性能提升到极致的;其中从很多方面(可变长度、字符串、集合等)解释了一些性能优化的技巧,值得每一个开发人员学习,特别是框架的开发人员的学习,一定能让大家获益匪浅。 ## 简介 我发

c# 如何将程序加密隐藏?

下面将介绍如何通过`LiteDB`将自己的程序进行加密,首先介绍一下`LiteDB`。 ## LiteDB LiteDB是一个轻量级的嵌入式数据库,它是用C#编写的,适用于.NET平台。它的设计目标是提供一个简单易用的数据库解决方案,可以在各种应用程序中使用。 LiteDB使用单个文件作为数据库存储