制作一个极简的 .NET 客户端应用自安装或自更新程序

栏目: ASP.NET · 发布时间: 5年前

内容简介:本文主要说的是 .NET 客户端应用,可以是只能在 Windows 端运行的基于 .NET Framework 或基于 .NET Core 的 WPF / Windows Forms 应用,也可以是其他基于 .NET Core 的跨平台应用。但是不是那些更新权限受到严格控制的 UWP / iOS / Android 应用。本文将编写一个简单的程序,这个程序初次运行的时候会安装自己,如果已安装旧版本会更新自己,如果已安装最新则直接运行。简单的安装过程实际上是

本文主要说的是 .NET 客户端应用,可以是只能在 Windows 端运行的基于 .NET Framework 或基于 .NET Core 的 WPF / Windows Forms 应用,也可以是其他基于 .NET Core 的跨平台应用。但是不是那些更新权限受到严格控制的 UWP / iOS / Android 应用。

本文将编写一个简单的程序,这个程序初次运行的时候会安装自己,如果已安装旧版本会更新自己,如果已安装最新则直接运行。

自安装或自更新的思路

简单的安装过程实际上是 解压 + 复制 + 配置 + 外部命令 。这里,我只做 复制 + 配置 + 外部命令 ,并且把 配置 + 外部命令 合为一个步骤。

于是:

  1. 启动后,检查安装路径下是否有已经安装的程序;
  2. 如果没有,则直接复制自己过去;
  3. 如果有,则比较版本号,更新则复制过去。

本文用到的知识

使用

于是我写了一个简单的类型用来做自安装。创建完 SelfInstaller 的实例后,根据安装完的结果做不同的行为:

  • 显示安装成功的窗口
  • 显示正常的窗口
  • 关闭自己
using System.IO;
using System.Windows;
using Walterlv.Installing;

namespace Walterlv.ENPlugins.Presentation
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            var installer = new SelfInstaller(Path.Combine(
                @"C:\Users\lvyi\AppData\Local\Walterlv",
                "Walterlv.ENPlugins.Presentation.exe"));

            var state = installer.TryInstall();
            switch (state)
            {
                case InstalledState.Installed:
                case InstalledState.Updated:
                    new InstallTipWindow().Show();
                    break;
                case InstalledState.Ran:
                    new MainWindow().Show();
                    break;
                case InstalledState.ShouldRerun:
                    Shutdown();
                    break;
            }
        }
    }
}

附全部源码

本文代码在 https://gist.github.com/walterlv/33bdd62e2411c69c2699038e2bc97488

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;

namespace Walterlv.Installing
{
    /// <summary>
    /// 自安装或字更新的安装器。
    /// </summary>
    public class SelfInstaller
    {
        /// <summary>
        /// 初始化 <see cref="SelfInstaller"/> 的新实例。
        /// </summary>
        /// <param name="targetFilePath">要安装的主程序的目标路径。</param>
        /// <param name="installingProcedure">如果需要在安装后执行额外的安装步骤,则指定自定义的安装步骤。</param>
        public SelfInstaller(string targetFilePath, IInstallingProcedure installingProcedure = null)
        {
            TargetFileInfo = new FileInfo(targetFilePath ?? throw new ArgumentNullException(nameof(targetFilePath)));
            InstallingProcedure = installingProcedure;
        }

        /// <summary>
        /// 获取要安装的主程序的目标路径。
        /// </summary>
        private FileInfo TargetFileInfo { get; }

        /// <summary>
        /// 获取或设置当应用重新启动自己的时候应该使用的参数。
        /// </summary>
        public string RunSelfArguments { get; set; } = "--rerun-reason {reason}";

        /// <summary>
        /// 获取此自安装器安装中需要执行的自定义安装步骤。
        /// </summary>
        public IInstallingProcedure InstallingProcedure { get; }

        /// <summary>
        /// 尝试安装,并返回安装结果。调用者可能需要对安装结果进行必要的操作。
        /// </summary>
        public InstalledState TryInstall()
        {
            if (!CheckRunningInNormalPrivilege())
            {
                // 降权处理。
                Process.Start("explorer.exe", BuildRerunArguments("Privilege", true));
                return InstalledState.ShouldRerun;
            }

            var state = InstallOrUpdate();
            switch (state)
            {
                // 已安装或更新,由已安装的程序处理安装后操作。
                case InstalledState.Installed:
                case InstalledState.Updated:
                case InstalledState.ShouldRerun:
                    Process.Start("explorer.exe", BuildRerunArguments(state.ToString(), true, TargetFileInfo.FullName));
                    return state;
            }

            state = InstallingProcedure?.AfterInstall() ?? InstalledState.Ran;
            return state;
        }

        /// <summary>
        /// 检查程序是否运行在普通用户权限下(此安装器仅限运行在普通权限)。
        /// </summary>
        private bool CheckRunningInNormalPrivilege()
        {
            var identity = WindowsIdentity.GetCurrent();
            var principal = new WindowsPrincipal(identity);
            return !principal.IsInRole(WindowsBuiltInRole.Administrator);
        }

        /// <summary>
        /// 进行安装或更新。执行后将返回安装状态以及安装后的目标程序路径。
        /// </summary>
        private InstalledState InstallOrUpdate()
        {
            var extensionFilePath = TargetFileInfo.FullName;
            var selfFilePath = Assembly.GetExecutingAssembly().Location;

            // 判断当前是否已经运行在插件目录下。如果已经在那里运行,那么不需要安装。
            if (string.Equals(extensionFilePath, selfFilePath, StringComparison.CurrentCultureIgnoreCase))
            {
                // 继续运行自己即可。
                return InstalledState.Ran;
            }

            // 判断插件目录下的软件版本是否比较新,如果插件目录已经比较新,那么不需要安装。
            var isOldOneExists = File.Exists(extensionFilePath);
            if (isOldOneExists)
            {
                var (currentVersion, installedVersion) = GetVersions();
                if (currentVersion <= installedVersion)
                {
                    // 运行已安装目录下的自己。
                    return InstalledState.ShouldRerun;
                }
            }

            // 将自己复制到插件目录进行安装。
            CopySelfToInstall();

            return isOldOneExists ? InstalledState.Updated : InstalledState.Installed;

            (Version currentVersion, Version installedVersion) GetVersions()
            {
                Version installedVersion;
                try
                {
                    var installed = Assembly.ReflectionOnlyLoadFrom(extensionFilePath);

                    var installedVersionString =
                        installed.GetCustomAttributesData()
                            .FirstOrDefault(x =>
                                x.AttributeType.FullName == typeof(AssemblyFileVersionAttribute).FullName)
                            ?.ConstructorArguments[0].Value as string ?? "0.0";
                    installedVersion = new Version(installedVersionString);
                }
                catch (FileLoadException)
                {
                    installedVersion = new Version(0, 0);
                }
                catch (BadImageFormatException)
                {
                    installedVersion = new Version(0, 0);
                }

                var current = Assembly.GetExecutingAssembly();
                var currentVersionString =
                    current.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "0.0";
                var currentVersion = new Version(currentVersionString);
                return (currentVersion, installedVersion);
            }
        }

        /// <summary>
        /// 将自己复制到目标安装路径。
        /// </summary>
        private void CopySelfToInstall()
        {
            var extensionFolder = TargetFileInfo.Directory.FullName;
            var extensionFilePath = TargetFileInfo.FullName;
            var selfFilePath = Assembly.GetExecutingAssembly().Location;

            try
            {
                if (!Directory.Exists(extensionFolder))
                {
                    Directory.CreateDirectory(extensionFolder);
                }

                File.Copy(selfFilePath, extensionFilePath, true);
            }
            catch (IOException)
            {
                File.Move(extensionFilePath, extensionFilePath + ".bak");
                File.Copy(selfFilePath, extensionFilePath, true);
            }
        }

        /// <summary>
        /// 生成用于重启自身的启动参数。
        /// </summary>
        /// <param name="rerunReason">表示重启原因的一个单词(不能包含空格)。</param>
        /// <param name="includeExecutablePath"></param>
        /// <param name="executablePath"></param>
        /// <returns></returns>
        private string BuildRerunArguments(string rerunReason, bool includeExecutablePath, string executablePath = null)
        {
            if (rerunReason == null)
            {
                throw new ArgumentNullException(nameof(rerunReason));
            }

            if (rerunReason.Contains(" "))
            {
                throw new ArgumentException("重启原因不能包含空格", nameof(rerunReason));
            }

            var args = new List<string>();

            if (includeExecutablePath)
            {
                args.Add(string.IsNullOrWhiteSpace(executablePath)
                    ? Assembly.GetEntryAssembly().Location
                    : executablePath);
            }

            if (!string.IsNullOrWhiteSpace(RunSelfArguments))
            {
                args.Add(RunSelfArguments.Replace("reason", rerunReason));
            }

            return string.Join(" ", args);
        }
    }

    /// <summary>
    /// 表示安装完后的状态。
    /// </summary>
    public enum InstalledState
    {
        /// <summary>
        /// 已安装。
        /// </summary>
        Installed,

        /// <summary>
        /// 已更新。说明运行此程序时,已经存在一个旧版本的应用。
        /// </summary>
        Updated,

        /// <summary>
        /// 已代理启动新的程序,所以此程序需要退出。
        /// </summary>
        ShouldRerun,

        /// <summary>
        /// 没有执行安装、更新或代理,表示此程序现在是正常启动。
        /// </summary>
        Ran,
    }
}

本文会经常更新,请阅读原文: https://walterlv.com/post/simple-windows-app-self-installer.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

制作一个极简的 .NET 客户端应用自安装或自更新程序 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接:https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)


以上所述就是小编给大家介绍的《制作一个极简的 .NET 客户端应用自安装或自更新程序》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法分析

数据结构与算法分析

维斯 / 人民邮电 / 2006-10 / 59.00元

《数据结构与算法分析:C++描述》秉承Weiss著全一贯的严谨风格,同时又突出了实践。书中充分应用了现代C++语言特性,透彻地讲述了数据结构的原理和应用,不仅使学生具备算法分析能力,能够开发高效的程序,而且让学生掌握良好的程序设计技巧。一起来看看 《数据结构与算法分析》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具