必须用独立进程完成替换,主程序只负责检测、启动updater后立即退出;因windows加载器锁定exe/dll句柄,file.copy或file.replace在文件被占用时必然失败,属操作系统级保护机制。

直接覆盖正在运行的 MyApp.exe 会失败,报错 System.IO.IOException: The process cannot access the file because it is being used by another process。必须用独立进程完成替换,主程序只负责检测、启动 updater、然后干净退出。
为什么不能在主进程中直接 File.Copy + File.Delete
Windows 加载器会锁定已映射的 EXE 和引用的 DLL 文件句柄,哪怕你调用 Application.Exit(),进程资源释放也有延迟;File.Copy 到原路径会立刻被系统拒绝,File.Replace 在目标被占用时也抛异常。这不是权限问题,是操作系统级保护机制。
- 常见错误现象:下载完新版本,一执行替换就卡住或崩溃,日志里反复出现“文件正被另一个进程使用”
- 真实场景中,用户双击桌面快捷方式启动程序,而更新器若没以相同权限(如管理员)运行,连写入
Program Files目录的权限都没有 - 即使绕过锁定强行写入(比如用批处理+计划任务延时),也无法保证 DLL 加载一致性,极大概率导致重启后
TypeLoadException或入口点找不到
如何正确启动 updater 并移交控制权
主程序检测到更新后,唯一安全的做法是启动一个外部 Updater.exe,传入自身路径,然后立即退出 —— 不要等它完成,也不要自己调用 Process.Start 拉起新版本。
- 启动命令示例:
Process.Start("Updater.exe", $"--app-path \"{Application.ExecutablePath}\" --restart-args \"{string.Join(" ", Environment.GetCommandLineArgs(), 1)}\"") - 主程序退出前必须调用
Application.Exit()(WinForms)或Environment.Exit(0),避免消息循环残留导致句柄未释放 - 切勿用
Process.Start("MyApp.exe")后再退出 —— 新进程可能还没加载完,旧进程就释放了工作目录锁,导致后续替换失败 - updater 启动后应检查是否具备目标路径写权限,若无则主动弹出 UAC 提权(通过清单声明
requireAdministrator或运行时调用ShellExecute以管理员身份重启自身)
下载与替换必须满足原子性 + 可回退
临时文件不校验、直接覆盖原 EXE,等于把用户程序的命运交给网络和磁盘——一次断电或中断就能让软件彻底无法启动。
- 下载必须先写入临时路径:
Path.GetTempFileName()生成唯一名,例如tmpA1B2.exe,而非直接往MyApp.exe写 - 下载完成后必须校验服务端提供的
SHA256值:SHA256.HashData(File.ReadAllBytes(tempPath)),不匹配就File.Delete(tempPath)并返回错误码 - 替换操作必须用
File.Replace(newPath, originalPath, backupPath):它在 NTFS 上是原子操作,且自动备份原文件到backupPath(如MyApp.exe.bak),出问题可秒级恢复 - 替换完成后不要立刻
Process.Start,加await Task.Delay(150)确保 NTFS 元数据刷新、所有句柄真正关闭,否则新进程仍可能加载失败
HttpClient 下载更新包的坑与对策
用裸 WebClient 或随手 new 的 HttpClient 做更新,90% 的失败都发生在网络层:超时、重定向丢失、证书异常、进度回调跨线程崩 UI。
- 必须复用单例
HttpClient实例,避免 socket 耗尽;设置Timeout = TimeSpan.FromSeconds(90)和MaxResponseContentBufferSize = 100 * 1024 * 1024 - 对
HttpRequestException和TaskCanceledException做指数退避重试(最多 3 次),但跳过 4xx 响应(说明 URL 错或版本不存在) - 服务端若支持 ETag,请求头加上
If-None-Match,避免重复下载相同版本 - 进度回调必须走
IProgress<int></int>,别在回调里直接操作progressBar.Value——HttpClient的回调线程不是 UI 线程,会触发跨线程异常
真正容易被忽略的是:校验哈希值的服务端签名是否可信。本地开发时关掉证书验证(ServicePointManager.ServerCertificateValidationCallback = (a,b,c,d) => true)很爽,但上线后若没配 HTTPS 或没做服务端响应签名验证,中间人可以轻松替换下载包 —— 用户看到“更新成功”,实际运行的是恶意代码。这个环节没有捷径,必须上正规 CA 证书 + 服务端提供带签名的 checksum。











