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

2023-10-22 09:45:34 浏览数 (2)

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

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

自安装或自更新的思路

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

于是:

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

本文用到的知识

  • 在 Windows 系统上降低 UAC 权限运行程序(从管理员权限降权到普通用户权限) - walterlv
  • Windows 上的应用程序在运行期间可以给自己改名(可以做 OTA 自我更新) - walterlv
  • 仅反射加载(ReflectionOnlyLoadFrom)的 .NET 程序集,如何反射获取它的 Attribute 元数据呢? - walterlv

使用

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

  • 显示安装成功的窗口
  • 显示正常的窗口
  • 关闭自己

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

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(@"C:UserslvyiAppDataLocalWalterlv"); var state = installer.TryInstall(); switch (state) { case InstalledState.Installed: case InstalledState.Updated: case InstalledState.UpdatedInUse: new InstallTipWindow().Show(); break; case InstalledState.Same: case InstalledState.Ran: new MainWindow().Show(); break; case InstalledState.ShouldRerun: Shutdown(); break; } } } }

附全部源码

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; namespace Walterlv.EasiPlugins.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) { var assembly = Assembly.GetCallingAssembly(); var extensionName = assembly.GetCustomAttribute<AssemblyTitleAttribute>().Title; TargetFileInfo = new FileInfo(Path.Combine( targetFilePath ?? throw new ArgumentNullException(nameof(targetFilePath)), extensionName, extensionName Path.GetExtension(assembly.Location))); 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() { var state0 = InstallOrUpdate(); switch (state0) { // 已安装或更新,由已安装的程序处理安装后操作。 case InstalledState.Installed: case InstalledState.Updated: case InstalledState.UpdatedInUse: case InstalledState.Same: break; case InstalledState.ShouldRerun: Process.Start(TargetFileInfo.FullName, BuildRerunArguments(state0.ToString(), false)); return state0; } var state1 = InstallingProcedure?.AfterInstall(TargetFileInfo.FullName) ?? InstalledState.Ran; if (state0 is InstalledState.UpdatedInUse || state1 is InstalledState.UpdatedInUse) { return InstalledState.UpdatedInUse; } if (state0 is InstalledState.Updated || state1 is InstalledState.Updated) { return InstalledState.Updated; } if (state0 is InstalledState.Installed || state1 is InstalledState.Installed) { return InstalledState.Installed; } return state1; } /// <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 isNewer = CheckIfNewer(); if (!isNewer) { // 运行已安装目录下的自己。 return InstalledState.Same; } } // 将自己复制到插件目录进行安装。 var succeedOnce = CopySelfToInstall(); if (!succeedOnce) { // 如果不是一次就成功,说明目标被占用。 return InstalledState.UpdatedInUse; } return isOldOneExists ? InstalledState.Updated : InstalledState.Installed; bool CheckIfNewer() { Version installedVersion; try { var installed = Assembly.ReflectionOnlyLoadFrom(extensionFilePath); var installedVersionString = installed.GetCustomAttributesData() .FirstOrDefault(x => x.AttributeType.FullName == typeof(AssemblyFileVersionAttribute).FullName) ?.ConstructorArguments0.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 bool CopySelfToInstall() { var extensionFolder = TargetFileInfo.Directory.FullName; var extensionFilePath = TargetFileInfo.FullName; var selfFilePath = Assembly.GetExecutingAssembly().Location; if (!Directory.Exists(extensionFolder)) { Directory.CreateDirectory(extensionFolder); } var isInUse = false; for (var i = 0; i < int.MaxValue; i ) { try { if (i > 0) { File.Move(extensionFilePath, extensionFilePath $".{i}.bak"); } File.Copy(selfFilePath, extensionFilePath, true); return !isInUse; } catch (IOException) { // 不退出循环,于是会重试。 isInUse = true; } } return !isInUse; } /// <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> UpdatedInUse, /// <summary> /// 已代理启动新的程序,所以此程序需要退出。 /// </summary> ShouldRerun, /// <summary> /// 两个程序都是一样的,跑谁都一样。 /// </summary> Same, /// <summary> /// 没有执行安装、更新或代理,表示此程序现在是正常启动。 /// </summary> Ran, } }

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

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。

0 人点赞