Delphi 自动在线升级源码:独立升级器实现安全替换与回滚

发布时间:2026/10/24 3:30:55
Delphi 自动在线升级源码:独立升级器实现安全替换与回滚 简介Delphi自动在线升级程序源码面向使用 Object Pascal 开发桌面应用的开发者以及需要为既有 Delphi 项目补齐自动更新能力的团队帮助理解并落地版本检测、更新包下载、文件校验、静默替换与重启提示等完整流程。压缩包共 49 个文件约 3.61MB包含 dpr、pas、dfm 等工程与界面源码ini、cfg、xml 等配置及更新清单exe、dcu 等编译产物另有 fr3 报表模板和若干 rar 归档目录覆盖客户端配置、升级主程序与相关报表资源。已有 331 人学习适合作为中小型桌面软件升级模块的参考实现。读者可从中梳理网络通信、多线程下载、哈希校验和文件安全替换的实现思路并依据自身项目定制断点续传、增量更新或兼容性修复策略源码结构清晰也便于对照调试、异常回滚和二次开发快速搭建可运行的升级骨架。1. Delphi 自动在线升级源码把“程序装到别人机器上之后怎么换掉”这件事收回自己手里客户现场那台工控机上还跑着半年前的客户端一个字段名拼错就要派人出差做过桌面交付的都懂这种憋屈。手上这套Delphi 自动在线升级源码干的就是把这件事收回来主程序启动时读一份远端版本清单比对版本号有更新就下载、校验然后拉起一个独立的小升级器由它替换掉主程序再重新启动。整套东西不依赖任何安装框架纯 Delphi 写主程序 升级器两个 exeDelphi 7 到 XE 系列都能编译过。适合谁适合还在用 Delphi 维护内部管理系统、工控上位机、行业工具软件又不想为了一个自动更新去引入整套安装包方案的团队。它不解决打包和安装只解决“程序已经装在别人机器上之后怎么安全地换掉”这一段而这恰恰是最容易出事故的一段。下面按项目骨架、核心链路、跑通步骤、踩坑记录、二次开发五段拆。2. 项目骨架与技术选型两个 exe 加一份版本清单怎么分工2.1 目录结构主程序、升级器、清单文件各管一段这套源码的目录是典型的“双工程 共享单元”结构打开工程组能看到两个 dpr 和平级的公共代码目录DelphiAutoUpdate/ ├── MainApp/ # 主程序工程 │ ├── MainApp.dpr │ ├── uMain.pas # 主窗体负责触发检查 │ ├── uUpdateClient.pas # 检查版本 下载 拉起升级器 │ └── MainApp.res ├── Updater/ # 升级器工程独立 exe │ ├── Updater.dpr │ ├── uUpdaterMain.pas # 等待、备份、替换、回滚、重启 │ └── Updater.res ├── Common/ # 两个工程共享 │ ├── uVersion.pas # 版本号比对 │ ├── uHash.pas # MD5 校验 │ ├── uNet.pas # Indy 下载线程封装 │ └── uLog.pas # 文件日志排查全靠它 ├── Server/ │ ├── update.ini # 远端版本清单样例 │ └── package/ # 待发布的整包文件 └── README.md职责切得很干净MainApp永远只负责“发现有没有新版、把新版拉下来、把升级器叫起来”Updater永远只负责“等旧进程死透、把文件换掉、把新进程拉起来”。两个 exe 之间不共享内存、不共享句柄唯一的通信方式是命令行参数。这种设计看起来朴素但它把最容易崩的那一步隔离到了最短的代码路径上——升级器里只有两百多行出了问题一眼能看完。Common目录里那几个单元是两边都要用的尤其是uVersion.pas和uLog.pas。版本比对逻辑被主程序和升级器共用避免出现“主程序觉得 2.3.1.7 比 2.3.10.1 大、升级器判断相反”这种自己打自己的情况。日志单元更关键升级过程中的每一步都要落盘因为客户端在现场、开发在办公室出问题时日志就是唯一的黑匣子。2.2 为什么用独立升级器进程而不是在主程序里自己换自己这是整个项目的技术核心决策值得单独说清楚。Windows 下正在运行的 exe 文件是被独占锁定的主程序想覆盖自己的 exeCopyFile必然返回失败GetLastError一般是 32文件被另一个进程使用。常见的错误做法是“先重命名自己再写入新文件”这在某些系统上能凑合跑但只要程序里有任何延迟加载的 DLL、或者杀软正在扫描就会在半路上翻车留下一个既不能运行、又没被替换干净的残骸。合格的做法是把替换动作交给第三方进程。主程序把下载好的新版本文件放在临时目录然后ShellExecute起Updater.exe把三个参数传过去主进程 PID、目标文件路径、新文件路径。随后主程序调用Application.Terminate主动退出释放文件锁。升级器拿到 PID 后用OpenProcess(SYNCHRONIZE)WaitForSingleObject等它真的退出再动手替换。提示等待必须带超时一般 30 秒不能让升级器无限等下去。主程序如果卡在某个OnCloseQuery里不肯退升级器要有能力判定超时并放弃本次升级把临时文件保留下来供下次重试而不是硬替换。硬替换在文件仍被占用时会得到一个零字节的 exe那就真成砖了。这里还有个容易被忽略的细节升级器本身也放在程序目录里如果本次发布恰好也要更新Updater.exe那升级器替换自己同样会失败。源码里的处理方式是升级器只替换“除自己以外”的文件自己的更新走下一次启动——先落地一个新版本号下次升级时由旧升级器去换新升级器形成接力。这个逻辑不复杂但如果没有提前想清楚第一次发升级器改动就会卡住。2.3 版本清单 update.ini 的字段设计与约束远端清单用 Ini 而不是 JSON原因是 Delphi 自带TIniFile零依赖、零第三方库老项目里连 SuperObject 都不想引。字段设计如下字段类型说明约束versionstring最新版本号四段式必须能被CompareVersion解析禁止带字母后缀urlstring新版本文件的下载地址支持 http/https必须是直链md5string新文件的 MD5小写 32 位下载完成后强校验不一致就丢弃sizeint64文件字节数用于下载前预检磁盘空间、下载后比对forcebool是否强制升级为 1 时不允许用户点“稍后”minverstring允许升级的最低版本低于它说明版本跨度太大走完整包重装notestring更新说明多行用\n转义直接显示在升级提示框里样例[update] version2.3.1.7 urlhttp://192.168.1.10:8000/package/app_2317.zip md59f2c1a7d4b8e3f60c5a1d2e7b4f80931 size18432000 force0 minver2.0.0.0 note1. 修复导出报表偶发崩溃\n2. 新增批量导入入口size这个字段很多人觉得多余MD5 已经能保证完整性了。但实际用下来它很有价值下载前可以先判断客户端剩余磁盘空间够不够不够就直接提示用户清理而不是下到一半报 IO 错误下载过程中还能拿它当进度条的分母避免依赖Content-Length有些反向代理会改写或干脆不返回这个头。minver是我个人建议一定要留的字段。真实项目里经常出现版本跨度过大的情况——客户机器上还是 1.x而服务端已经跑到 3.x中间隔了十几版数据库结构变更。这时候只替换 exe 是没用的数据库脚本对不上照样起不来。源码里对这种情况的处理是低于minver就提示“版本过旧请联系技术支持获取完整安装包”不再走自动升级流程。这条规则挡掉过好几次事故。3. 核心链路拆解从版本比对到进程替换的四段代码3.1 版本号比对别用字符串比大小先说个血泪教训。我见过一个项目直接拿2.3.1.7 2.3.10.1做字符串比较结果 2.3.10.1 发布之后全网客户端纹丝不动——因为字符串比较里1小于7程序认为本地版本更新。这种 bug 上线后毫无征兆日志里一切正常就是没升级。正确做法是按段拆开转成整数逐段比// 比较两个形如 1.2.3.4 的版本号 // 返回值AV1 AV2 返回 1相等返回 0小于返回 -1 function CompareVersion(const AV1, AV2: string): Integer; var L1, L2: TStringList; I, N1, N2: Integer; begin Result : 0; L1 : TStringList.Create; L2 : TStringList.Create; try L1.Delimiter : .; L1.StrictDelimiter : True; // 关键只按点切别再按空格切 L1.DelimitedText : Trim(AV1); L2.Delimiter : .; L2.StrictDelimiter : True; L2.DelimitedText : Trim(AV2); for I : 0 to Max(L1.Count, L2.Count) - 1 do begin N1 : 0; N2 : 0; if I L1.Count then N1 : StrToIntDef(Trim(L1[I]), 0); if I L2.Count then N2 : StrToIntDef(Trim(L2[I]), 0); if N1 N2 then begin if N1 N2 then Result : 1 else Result : -1; Exit; end; end; finally L1.Free; L2.Free; end; end;几个参数要说明清楚。StrictDelimiter : True必须设否则TStringList在解析DelimitedText时除了点号还会按空格切分版本号里只要混进一个空格就解析出多余的空段。StrToIntDef(..., 0)而不是StrToInt是为了容忍2.3.1.beta这种脏数据——转不出来的段按 0 处理不会抛异常中断整个检查流程。循环上界用Max(L1.Count, L2.Count)而不是L1.Count是为了处理2.3和2.3.0.1这种情况。按短的循环会认为两者相等实际后者更新。短的一方缺段补 0这才是符合直觉的语义。注意版本号统一成四段式后再发布。项目里version字段虽然有容错但一旦出现2.3.1和2.3.1.0混用minver的比较边界就会变得很难解释运维排查时也容易看花眼。3.2 下载线程断点续传与进度回调主程序不能在主线程里同步下载否则界面直接假死用户以为程序挂了就强杀进程。源码里用一个TThread子类包住 Indy 的TIdHTTPtype TDownloadThread class(TThread) private FUrl, FSavedFile: string; FHttp: TIdHTTP; FOnProgress: TProgressEvent; // 自定义回调已下载 / 总大小 protected procedure Execute; override; procedure HttpWork(Sender: TObject; AWorkMode: TWorkMode; const AWorkCount: Int64); public constructor Create(const AUrl, ASavedFile: string); property OnProgress: TProgressEvent read FOnProgress write FOnProgress; end; procedure TDownloadThread.Execute; var StartPos: Int64; FS: TFileStream; begin FHttp : TIdHTTP.Create(nil); FS : nil; try FHttp.ConnectTimeout : 8000; // 连不上就别干等 FHttp.ReadTimeout : 30000; // 单次读超时防半死不活的连接 FHttp.OnWork : HttpWork; // 断点续传临时文件已存在则从尾部继续 StartPos : 0; if FileExists(FSavedFile) then StartPos : GetFileSizeByName(FSavedFile); // 项目里的工具函数 if StartPos 0 then begin FHttp.Request.Range : IntToStr(StartPos) -; FS : TFileStream.Create(FSavedFile, fmOpenWrite or fmShareDenyNone); FS.Position : StartPos; // 必须显式定位否则从头覆盖 end else FS : TFileStream.Create(FSavedFile, fmCreate); try FHttp.Get(FUrl, FS); // 阻塞直到本线程下载结束 finally FS.Free; end; finally FHttp.Free; end; end;这里有两个参数直接影响体验。ConnectTimeout给 8 秒ReadTimeout给 30 秒——不是随便填的前者覆盖“DNS 解析慢 服务器握不上手”后者覆盖“连上了但服务端半天不吐数据”。两者都设成 60 秒以上用户点升级后就是干等一分钟毫无反馈体验上等同于卡死。fmOpenWrite or fmShareDenyNone和fmCreate的区分是断点续传的关键。用fmCreate打开已存在的文件会直接截断成 0 字节续传就成了重新下载而用fmOpenWrite之后如果不显式设置Position : StartPos写入位置默认在 0会把文件头覆盖掉最终得到一个“长度翻倍、内容错乱”的怪物文件。这个坑我在别的项目里踩过表现出来的现象是“下载 100% 但 MD5 永远不对”。OnWork回调里不要直接更新 VCL 控件要用Synchronize或者TThread.Queue切回主线程否则在多核机器上偶发访问冲突属于那种“测十次挂一次”的玄学问题。进度条的分母优先用清单里的size拿不到再退回FHttp.Response.ContentLength。3.3 完整性校验MD5 不过就不许换下载完成不等于下载正确。中间可能经过代理缓存、可能服务端配错返回了一个 HTML 错误页HTTP 状态码还是 200也可能是磁盘写满导致文件被截断。落地前必须校验一遍// 计算文件 MD5返回小写十六进制字符串 function GetFileMD5(const AFileName: string): string; var MD5: TIdHashMessageDigest5; FS: TFileStream; begin MD5 : TIdHashMessageDigest5.Create; FS : TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); try Result : LowerCase(MD5.HashStreamAsHex(FS)); finally FS.Free; MD5.Free; end; end; // 校验入口大小先比再比 MD5 function VerifyPackage(const AFile, AExpectMd5: string; AExpectSize: Int64): Boolean; var ActualMd5: string; begin Result : False; if not FileExists(AFile) then Exit; if (AExpectSize 0) and (GetFileSizeByName(AFile) AExpectSize) then begin Log.Warn(包大小不符期望 %d 实际 %d, [AExpectSize, GetFileSizeByName(AFile)]); Exit; end; ActualMd5 : GetFileMD5(AFile); if not SameText(ActualMd5, AExpectMd5) then begin Log.Warn(MD5 不符期望 %s 实际 %s, [AExpectMd5, ActualMd5]); Exit; end; Result : True; end;HashStreamAsHex对大文件是流式读取的不会一次性把 18MB 全灌进内存这点对工控机那种内存吃紧的环境很重要。先比大小再比 MD5 是刻意的顺序大小不符的时候根本没必要去算哈希省一次全文件读取。校验失败的处理策略要写死删掉临时文件记一条 warn 日志向用户提示“更新包校验失败请稍后重试”然后不进入替换流程。绝不能“校验失败但先试试看”——一个坏掉的 exe 换上去客户现场就直接停工了而停留旧版本最多是功能少一点这个取舍没有讨论空间。3.4 升级器等进程退出、备份、替换、回滚、重启Updater.exe的逻辑是整条链路里最短也最要命的一段。它启动后从命令行拿三个参数然后按固定顺序执行// 启动参数Updater.exe /pid:1234 /target:D:\App\MainApp.exe // /new:C:\Users\xx\AppData\Local\Temp\MainApp_2317.exe procedure TfrmUpdater.Run; var H: THandle; BackupFile: string; begin // 1. 等旧进程彻底退出最多 30 秒 if FPid 0 then begin H : OpenProcess(SYNCHRONIZE, False, FPid); if H 0 then try if WaitForSingleObject(H, 30000) WAIT_TIMEOUT then begin Log.Warn(等待旧进程超时放弃本次升级); Exit; end; finally CloseHandle(H); end; end; // 2. 备份旧文件文件名带时间戳回滚全靠它 BackupFile : FTarget .bak_ FormatDateTime(yyyymmddhhnnss, Now); if FileExists(FTarget) then CopyFile(PChar(FTarget), PChar(BackupFile), False); // 3. 替换失败立刻用备份还原 if not CopyFile(PChar(FNewFile), PChar(FTarget), False) then begin Log.Error(替换失败 SysErrorMessage(GetLastError)); if FileExists(BackupFile) then CopyFile(PChar(BackupFile), PChar(FTarget), False); Application.Terminate; Exit; end; // 4. 清理临时包重新拉起主程序 DeleteFile(FNewFile); ShellExecute(0, open, PChar(FTarget), nil, PChar(ExtractFileDir(FTarget)), SW_SHOWNORMAL); Application.Terminate; end;参数含义/pid是主进程 ID/target是最终要覆盖的文件全路径/new是下载到临时目录的新文件。三个都用双引号包住传因为C:\Program Files\...这种路径带空格不加引号会被拆成两个参数——这类低级错误在日志里表现为“target 参数为空”排查时先看日志里打印出来的完整命令行。OpenProcess(SYNCHRONIZE, ...)只需要同步权限不需要PROCESS_ALL_ACCESS权限越小越不容易被安全软件盯上。WaitForSingleObject返回WAIT_OBJECT_0表示进程真的退出了返回WAIT_TIMEOUT就必须放弃不能硬着头皮覆盖。备份文件名的yyyymmddhhnnss后缀不能省。早期我图省事用固定名.bak结果连续两次升级失败时第二次的备份把第一次的备份覆盖了原本能回滚的好文件没了。带时间戳之后D:\App\下最多会堆几个MainApp.exe.bak_20240115103022虽然丑但出事时能救命。最后一步ShellExecute的工作目录要显式传ExtractFileDir(FTarget)。如果传空新进程的工作目录会继承升级器所在的目录一旦主程序里有相对路径读配置很多 Delphi 老项目都有就会读不到文件表现为“升级后配置文件全丢了”——实际文件在原地只是它找错了地方。4. 跑起来本地搭一套能复现的升级环境4.1 编译顺序与输出目录约定两个工程要编译到同一个输出目录因为升级器必须和主程序并排放在一起主程序才能在启动升级时用ExtractFilePath(Application.ExeName) Updater.exe找到它。Delphi 的工程选项里把MainApp和Updater的DCC/Delphi Compiler → Output directory都设成..\..\Bin\Unit output directory设成..\..\Bin\dcu\。如果用的是较老的 IDE 版本对应的是Project → Options → Directories/Conditionals里的Output directory。Bin/ ├── MainApp.exe ├── Updater.exe ├── MainApp.exe.bak_20240115103022 # 升级后才有 └── dcu/ # 编译中间产物不用发布编译顺序无所谓但发布顺序有所谓第一次上线时两个 exe 要一起发之后如果只改主程序就只更新MainApp.exe如果升级器逻辑也改了记得前面提到的“接力”规则——旧升级器换不了新升级器自己得靠下一次升级完成替换或者让用户手工覆盖一次。提示Bin目录里不要放.dcu、.map、.stat这些编译中间文件。客户端机器上多出来的文件不会影响运行但升级时如果整个目录做 MD5 比对就会出问题而且用户看到一堆陌生文件会来问。4.2 用一条命令起更新服务器本地验证不需要真的架一台服务器任何静态文件服务都行。把Server目录当成 web 根目录# 进入放 update.ini 和 package 的目录 cd /data/upgrade # Python 3 自带静态服务默认支持 Range 请求 python3 -m http.server 8000 # 验证清单能取到 curl -i http://127.0.0.1:8000/update.ini # 验证包能下载且支持断点续传关键看 206 状态码 curl -i -H Range: bytes100-200 http://127.0.0.1:8000/package/app_2317.zip第二条 curl 一定要跑。返回206 Partial Content说明服务端支持 Range断点续传可用返回200 OK且带完整文件内容说明服务端忽略了 Range 头这时客户端的续传逻辑会把整个文件追加到临时文件尾部得到一个超大且 MD5 必然错误的文件。源码里对此做了防御——校验失败就删包重下——但知道这个前提能让排查快很多。Windows 上如果不想装 Python用 IIS 的静态目录或者任意一个 HTTP 服务都行只要保证update.ini不带强缓存。清单文件必须每次都取最新所以服务端要给.ini配Cache-Control: no-cache否则客户端读到的永远是上一次的版本号表现为“发了新版但没人升”。4.3 触发一次升级并验证四个观察点把update.ini里的version改成比本地高一段url指向本地服务md5可以用命令行算md5sum package/app_2317.zip # Windows: certutil -hashfile package\app_2317.zip MD5然后启动MainApp.exe正常情况下会依次发生四件事每一件都可以独立验证第一界面弹出更新提示框内容来自note字段force0时带“稍后”按钮。如果没弹先看update.ini的 URL 是否可达再看日志里打印的本地版本号和远端版本号。第二进度条从 0 走到 100%。如果瞬间到 100%多半是服务端返回了一个几百字节的错误页大小字段对不上日志里会出现“包大小不符”。第三主程序窗口消失大约 13 秒后重新出现。中间这段时间是Updater.exe在跑它有自己的小窗口或干脆无界面可以在任务管理器里看到。第四升级后主程序的“关于”里版本号变成新的Bin目录下多了MainApp.exe.bak_时间戳临时目录里的新文件已被删除。四个点里任何一个断掉就先看uLog.pas落盘的日志文件一般写在%LOCALAPPDATA%\你的程序名\update.log。日志里按[检查]、[下载]、[校验]、[替换]分阶段打印比对着看能很快定位是哪一段没走通。4.4 上线前要确认的三件事第一是权限。如果程序装在C:\Program Files\下替换MainApp.exe需要管理员权限普通用户跑的升级器CopyFile会直接返回拒绝访问。处理方式是用ShellExecute的runas动词拉起升级器触发 UAC 提权ShellExecute(0, runas, PChar(UpdaterPath), PChar(Params), PChar(ExtractFilePath(UpdaterPath)), SW_SHOWNORMAL);代价是每次升级弹一次 UAC 框。如果装在%LOCALAPPDATA%或自定义目录很多行业软件都这么干就不需要提权体验更顺。这个取舍要在产品层面定别等上线了才发现客户现场全是受限账户。第二是回滚预案。升级器已经做了自动回滚替换失败用备份还原但有一种情况它管不了新 exe 换成功了但新版本自己启动就崩。这时候Updater已经退出没人能把旧版本换回来。补救办法是主程序启动时写一个“启动成功标记”正常初始化完成后才落盘升级器在拉起新进程后等 10 秒如果标记没更新就认为新版本有问题自动还原备份并拉起旧版本。这个机制源码里留了扩展点但默认没开生产环境建议打开。第三是日志与临时文件的清理。临时包在替换成功后会被删掉但失败时是保留的方便重试。保留策略要设上限比如只留最近三个否则客户机器上攒了几百兆的旧包运维会被投诉。5. 避坑与排查升级现场最容易翻车的五个点5.1 替换阶段的两个高频故障现象一升级后主程序启动即报“找不到 xxx.bpl”或直接闪退回滚也没有用。原因是新版本不只是一个 exe还带了一组 DLL/BPL而清单里只写了 exe 的地址升级器只替换了主程序。exe 和配套库版本对不上加载时找不到导出符号就崩了。解决办法是别把升级包做成裸 exe做成 zip 整包清单里加一个[files]段列出包内所有文件及其相对路径升级器解压后按列表逐个替换任何一个失败就整批回滚。整包替换比单文件替换多占几十兆带宽但能省掉一整类版本错配问题。现象二升级器提示“拒绝访问”或者替换后文件变成 0 字节。两个可能的原因要分开看。一是主程序没真正退出——托盘图标还在、隐藏的模态窗体挡住了OnClose、或者用户开了两个实例而升级器只等了其中一个的 PID。表现是WaitForSingleObject返回了但文件仍被另一个进程锁着。处理方式是在主程序里加单实例互斥量CreateMutex检查ERROR_ALREADY_EXISTS升级前先枚举同名进程全部结束。二是权限不足前面说过装Program Files下就必须runas。区分方法很简单看日志里的GetLastError5 是权限32 是占用。5.2 下载与校验阶段的三个高频故障现象三下载进度到 100% 但 MD5 永远不对或者文件大小正好是期望值的两倍。前者多半是服务端返回了 HTML 错误页——URL 拼错、文件被移走、反向代理返回了 404 页面但状态码是 200客户端照单全收。后者几乎可以确定是断点续传翻车服务端不支持Range返回了完整文件而客户端仍然从StartPos位置往后追加于是文件 前半段 完整文件。解决办法是在Execute里判断FHttp.Response.ResponseCode不是 200/206 就直接失败同时在开启续传前先用一个HEAD请求探测服务端是否返回Accept-Ranges: bytes不支持就删掉临时文件从头下。现象四第一次升级成功第二次升级时临时目录里全是旧包磁盘被占满。临时文件名如果固定成MainApp_new.exe失败一次之后就永远残留而重试逻辑看到文件已存在又走了断点续传于是拿着上一次的残包去续这一次的下载内容彻底对不上。正确做法是临时文件名带版本号MainApp_2317.exe升级器启动时顺带清理超过 7 天的临时文件主程序启动时也清理一次自己目录下的过期.bak_*。现象五升级器被安全软件拦截或直接删除用户点升级毫无反应。未签名的 exe 从临时目录启动在不少企业终端上会被静默拦截ShellExecute返回的句柄看起来正常但进程根本没起来主程序已经退出用户看到的就是“程序关了就没再打开”。排查方法是让主程序在ShellExecute之后等 3 秒检查升级器进程是否存在不存在就弹一个明确的提示“升级程序未能启动请检查安全软件”而不是干等着。长期方案是给两个 exe 都做代码签名这一步在正式交付前就该做越晚做越麻烦。注意所有排查都建立在有日志的前提上。升级链路的每一步读清单、比对版本、开始下载、下载完成、校验结果、拉起升级器、等待结果、替换成功/失败都必须落一行日志且带时间戳和关键参数。这条习惯我是在一次“客户说点了升级没反应、现场又没有远程”的事故之后才养成的。6. 二次开发把升级器改成贴合自己项目的形态这套源码留的扩展点比较足常见的改造方向有这么几个按投入产出比排序改造点改动位置大致工作量风险提示增加升级通道正式/灰度uUpdateClient.pas里把清单 URL 按通道拼半天通道标识要能从配置读别硬编码整包 zip 增量替换Updater新增解压 [files]清单解析23 天解压要用临时目录别直接解到程序目录启动健康检查 自动回滚主程序写启动标记Updater轮询12 天判定阈值别设太短冷启动慢的机器会误判包签名验签uHash.pas旁边加 RSA 验签单元2 天公钥要内置在 exe 里别从网络取静默升级不弹任何界面uMain.pas里把提示框换成定时检查半天只在force0且用户设置允许时才静默增量替换是收益最大的一个。目前是整包下载一个 18MB 的包客户在弱网现场要下好几分钟。改成 zip 之后可以先下载一个只含变更文件的“差量包”由升级器解压到临时目录再逐个覆盖带宽能降到几百 KB。实现上要注意 zip 里的路径必须做安全校验..\开头的条目一律拒绝否则就是一个典型的路径穿越漏洞——虽然是内部系统但这种口子留着迟早出事。解压建议直接引System.ZipXE2 以后自带Delphi 7 的话用TZipFile的替代实现或者外挂unzip别自己去解析 zip 结构。健康检查那一条我建议一定要做。具体做法是主程序在OnCreate全部完成、主窗体Show之后往%LOCALAPPDATA%写一个last_boot_ok.ini内容是版本号和时间戳升级器拉起新进程后循环等这个文件的版本号变成新版本最多等 15 秒等不到就还原备份、重新拉起旧版本并把新版本标记为“坏包”写进本地黑名单下次检查更新时直接跳过这个版本号。这套机制加上之后最坏情况从“客户现场停工等支援”变成了“自动退回旧版本用户只觉得刚才闪了一下”。验证改造是否有效别靠肉眼。我自己固定跑三个用例把服务端的包故意改坏一个字节确认客户端校验失败且不替换把新版本 exe 换成一个启动就Halt的空壳确认 15 秒后自动回滚到旧版本把网络在中途断掉确认续传能接着下完且 MD5 正确。三个用例跑通这套升级链路才算能交付。最后说个教训。早期版本我图省事把升级器的备份目录设成了%TEMP%觉得反正临时文件系统会清理。结果有个客户的机器上装了某种会自动清临时目录的优化工具升级到一半备份被清掉恰好那次替换又失败了回滚时找不到备份主程序当场没了最后是远程连过去手工拷回去的。从那以后我每次做这类“替换自身”的功能都强制走一遍“断网 只读目录 强杀进程”的破坏性测试三个场景全过才允许发版。希望这些经验能帮你少走点弯路这套源码拿去改改基本能撑起一个中小型 Delphi 项目的自动更新需求。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询