自动更新是Windows桌面程序的维护底线,需基于build_time优先比对版本、延时替换主程序、安全迁移配置文件,并确保子进程终止与启动参数还原。

Windows 桌面程序用 C# 写完发给用户,手动发新版 ZIP、让用户自己覆盖文件——这条路走不远。自动更新不是“锦上添花”,而是维护底线:补安全漏洞、修崩溃 bug、避免用户卡在旧版里连不上服务。它能跑起来,不等于它可靠;很多看似“成功更新”的程序,其实悄悄漏掉了 app.config 同步、没处理正在运行的子进程、或在 UAC 提权后丢了原始启动参数。
如何判断当前版本是否需要更新(别只比对 AssemblyVersion)
硬编码比对 AssemblyVersion 或靠服务器返回一个字符串版本号,极易出错。真实场景中,你可能修复了 bug 但没改版本号(热修),也可能用了语义化版本但客户端解析逻辑不一致。
- 服务端必须返回完整元数据:含
version(语义化格式如2.1.3)、build_time(ISO8601 时间戳)、file_hash(全量包或差分包的 SHA256)、download_url - 客户端比对优先级应为:
build_time>version>AssemblyVersion。时间戳最客观,避免开发时本地反复打包导致版本号乱跳 - 不要在 UI 线程直接调用
HttpClient.GetAsync,用await Task.Run(() => CheckUpdateAsync())防止界面冻结;超时设为 15 秒,失败后允许用户手动重试
下载和替换主程序时如何避免“正在使用”错误
Windows 锁定正在运行的 EXE 文件是常态。直接 File.Copy 到原路径必抛 IOException: The process cannot access the file。绕过方法不是“多试几次”,而是让系统接管替换时机。
- 下载新版本到临时目录(如
Path.Combine(Path.GetTempPath(), "MyAppUpdate", Guid.NewGuid().ToString())),解压后验证file_hash - 用
Process.Start("cmd.exe", $"/c timeout /t 1 /nobreak & move /y \"{newExePath}\" \"{currentExePath}\" & start \"\" \"{currentExePath}\"")启动批处理延时替换 —— 利用 cmd 进程退出后释放句柄的特性 - 更稳妥的做法是启用“双进程更新器”:主程序启动一个独立的
Updater.exe(无 UI,仅做复制+重启),自身调用Application.Exit()后由 updater 完成替换并重新拉起主程序
更新后配置文件(app.config / settings.settings)怎么安全迁移
用户修改过的 app.config 或 user.config 一旦被新包里的默认配置覆盖,就等于丢失所有个性化设置。这不是“备份一下再覆盖”能解决的——有些键值是运行时生成的(如加密连接串),不能简单合并。
- 永远不覆盖
app.config:只把新版本中的app.config.deploy(带 .deploy 后缀的模板)随包下发,更新器检查本地是否存在自定义配置,若存在则跳过写入 - 用户级配置(
Properties.Settings.Default)必须调用Settings.Save()后再更新,否则内存中修改不会落盘;更新器启动前,主程序应确保已调用Save() - 敏感配置项(如 API key)建议从外部文件或注册表读取,而非硬塞进
app.config,避免更新时意外泄露或覆盖
自动更新最危险的盲区不在下载或替换,而在“更新完成”的定义:是不是所有子进程(如嵌入的 Chrome 插件、后台托盘进程)都已终止?重启后的主程序是否携带了原始命令行参数(比如用户双击某个 .xyz 文件触发的关联启动)?这些细节不处理,用户会发现“更新完了,但上次打开的文档没了”或者“右键菜单失效”。真正的稳定,藏在 shutdown 逻辑的每一行 Process.Kill() 和 Application.Restart() 的上下文里。


















