
简介一套Delphi自动升级源码面向C/S架构桌面应用开发者用于解决客户端程序版本迭代时的自动检测与升级问题免去人工分发安装包、版本不统一的烦恼。完整实现了前端自动升级模块涵盖轮询式版本检测、升级包下载、程序重启替换等核心流程检测间隔可按需配置并采用主程序与升级程序分离设计便于开发者快速集成到现有系统中。资源包共56个文件以pas源码、dfm窗体、dcu编译单元为主同时包含dproj/groupproj工程文件、res资源文件、skincfg皮肤配置以及可直接运行的exe示例结构清晰压缩包仅3.52MB。借助内置的update与TestUpdate两套示例工程开发者可直观对照升级程序与主程序的交互细节快速完成二次开发或功能裁剪。目前已有668人学习下载适合需要为Delphi项目增加自动升级能力的初中级开发人员参考使用。1. 自动升级源码里那条轮询链路的工程价值在C/S系统里业务代码写得再仔细也逃不过一个现实线上总有用户跑旧版本新功能用不上、旧Bug修不到。这套Delphi自动升级源码处理的正是发布链路的最后一公里——客户端按设定间隔轮询升级服务器发现版本落后就下载升级包再启动独立升级程序替换文件。和启动时检查一次不同它用可配置间隔的持续轮询适合接口协议变更后强制旧客户端升级的场景。源码中update工程是独立升级器TestUpdate工程模拟主程序配合一个版本清单文件就能跑通全流程适合做二次改造。2. 升级服务器端的清单文件与版本号管理协议升级服务器端在这套源码里不是主角但整个升级链路能不能可靠七成取决于服务器端约定。以下方案是按C/S项目里最常见的做法补全的BS后台负责上传升级包、维护版本号客户端只认一个固定的清单文件。清单文件把当前最高版本、最低可升级版本、文件列表、校验值一次性暴露给客户端轮询逻辑才有得比对。2.1 版本清单的JSON字段约定清单文件放在升级服务器的固定路径下我一般命名为version.json。客户端升级程序启动后会先拉取这个文件再和自身携带的版本号做比对。字段设计不能只放一个大版本号因为增量升级时还要知道哪些旧版本可以直接升、哪些必须强制升。一个能用的最小结构是这样{ version: 2.3.1, minVersion: 2.0.0, releaseDate: 2026-01-15, force: true, files: [ { path: app/main.dll, md5: 9c2db10e6c4f31a8b7a6c5d4e3f2a1b0, size: 154624 }, { path: app/menu.xml, md5: 5f1e2d3c4b5a69788796a5b4c3d2e1f0, size: 2048 } ] }version字段是升级后的目标版本客户端拿到它和本地常量做比较。minVersion界定升级门槛低于这个值的旧客户端不允许继续使用检查到后直接弹强制更新提示。force字段决定非强制版本是提示后跳过还是弹窗必须升。files列表按相对路径列出升级包包含的文件md5用于下载完成后的完整性校验size用于进度条计算和磁盘空间预检。这里有一个容易踩的坑如果客户端用的是Delphi 2007之前的AnsiStringJSON里出现中文描述时编码不对会导致解析错乱。建议清单文件一律以UTF-8无BOM形式发布并在客户端解析前统一转换。服务器端写文件时不要用记事本默认的ANSI编码IIS或Nginx返回时也要带上charsetutf-8。在Delphi里用TJSONObject解析时中文路径会以Unicode字符串保存和服务器返回的UTF-8字节流比对时容易踩坑可以在解析后统一规范化路径分隔符。提示版本号字段建议统一为三段式主版本.次版本.修订号避免2.2.10和2.2.9这类字符串比较产生假阳性。用TStringList按.切分成整数后再逐段比对比直接CompareStr可靠。2.2 升级包文件哈希与目录结构服务器端文件目录和清单里的files.path必须一一对应不能依赖中文路径或带空格目录。我一般把升级服务器目录组织成upgrade-server/ version.json packages/ app/main.dll app/menu.xmlpackages目录和JSON中path字段拼接后就是下载地址。计算MD5在Delphi里用Indy的IdHashMessageDigest单元一段可复用的函数如下uses System.Classes, System.SysUtils, IdHashMessageDigest; function TFileHash.MD5OfFile(const AFileName: string): string; var LFileStream: TFileStream; LMD5: TIdHashMessageDigest5; begin LFileStream : TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); try LMD5 : TIdHashMessageDigest5.Create; try Result : LowerCase(LMD5.HashStreamAsHex(LFileStream)); finally LMD5.Free; end; finally LFileStream.Free; end; end;VCL和FMX项目都能直接用这段代码唯一要求是Indy组件版本一致。函数里LowerCase是为了统一十六进制串大小写Windows和Linux上生成的结果都是小写和服务器端对MD5时不用再考虑大小写差异。服务器端生成清单时遍历packages目录算出每个文件MD5再把结果写进version.json。如果团队里有其他语言生成清单也可以用命令行快速验证md5sum packages/app/main.dll哈希的作用是验证文件没被截断或者写坏而不是做安全防篡改。MD5在网络链路上可以被构造碰撞要防恶意替换必须靠签名机制第5章会补上这部分。服务器和客户端如果只是内网直连哈希校验已经够用。2.3 BS后台维护版本号与灰度窗口摘要里提到的BS页面在项目里落地后只需要三个接口上传升级包、发布版本、查询当前版本。客户端只依赖查询接口前两个接口仅管理员可见。一张表把接口契约固定下来接口方法行为客户端是否调用/api/version/currentGET返回当前version.json内容是/api/archive/uploadPOST上传文件到packages目录并计算MD5否/api/release/publishPOST更新version.json并覆盖旧版本号否灰度窗口不是每个项目都要求但如果服务器和客户端在同一个内网段建议至少在发布接口里增加一个releaseTime字段。客户端轮询到新版本时如果releaseTime晚于当前时间就继续等待而不是立刻升级。这样能把升级时刻从服务器端控制住防止刚上传完文件还没验证就大面积推给用户。这个字段在2.1的JSON里遗漏了的话可以在files同级补上客户端比对时多做一次时间判断。服务器端只要把这三个接口固定下来客户端的轮询逻辑就变得很简单拉JSON、比版本、决定是否下载三个动作顺序执行。接口响应里不要带多余的业务字段客户端解析失败时宁可跳过本轮升级也不要在主线程里弹错误框。3. uMain.pas与uDown.pas的轮询检测与下载逻辑拆解接下来是客户端核心。update工程里uMain.pas负责界面和轮询状态机uDown.pas负责具体下载。这两个单元的分工很明确uMain不问怎么下载uDown不管什么时候下载。3.1 uMain.pas的TTimer轮询触发机制uMain.pas的主窗体上放了一个TTimer它决定了检测频率。项目里默认间隔是60000毫秒也就是摘要里说的1分钟检测一次。这个值不是拍脑袋定的升级服务器带宽和客户端数量决定它的下限用户对弹窗的容忍度决定它的上限。procedure TfrmUpdate.FormCreate(Sender: TObject); begin FCurrentVersion : 2.2.9; FTimerCheck.Interval : 60000; // 60秒轮询一次 FTimerCheck.Enabled : True; end; procedure TfrmUpdate.TimerCheckTimer(Sender: TObject); begin FTimerCheck.Enabled : False; // 先停掉Timer防止重入 try if TUpdater.CheckNewVersion(FServerUrl) then TUpdater.DownloadAndInstall(FUpgradePackageUrl); finally FTimerCheck.Enabled : True; // 本轮逻辑跑完再恢复 end; end;这里有个关键动作每轮定时器触发后第一件事是把Enabled置为False。如果不这样做当CheckNewVersion内部执行时间超过1分钟TTimer的下一次触发就会排队造成并发检测。虽然TTimer本身不是多线程但重入会让状态机错乱比如下载过程中又触发一次检查两个下载流写同一个文件文件必然损坏。FCurrentVersion这个常量在实际项目里不应该手写而应该调用FileInfo.pas里的GetFileVersion函数从当前exe的文件版本信息区读取。这样每次用Delphi编译时带上版本资源主程序报出的版本号就和构建产物的版本保持一致避免忘了改常量导致升不上去。CheckNewVersion内部做的事可以归纳成三步拉version.json、解析JSON取version字段、和FCurrentVersion做整数化比较。返回True表示服务器版本更新。DownloadAndInstall建议同步执行因为下载期间用户看到的界面是正在升级请勿关闭不需要同时做别的事情。拉起独立升级进程的操作必须在下载完成之后进行。3.2 uDown.pas的文件下载实现uDown.pas里是下载逻辑使用Indy的TIdHTTP完成文件拉取。TIdHTTP比较适合这种下载完就走的短连接场景不需要引入额外的网络库。关键实现如下uses IdComponent, IdHTTP; function TUpdater.DownloadFile(const AUrl, AFileName: string; AOnProgress: TWorkEvent): Boolean; var LHttp: TIdHTTP; LFileStream: TFileStream; begin Result : False; LHttp : TIdHTTP.Create(nil); try LHttp.OnWork : AOnProgress; // 进度回调驱动进度条 LHttp.Request.UserAgent : AutoUpdater/2.0; LHttp.Request.Accept : */*; LHttp.ConnectTimeout : 5000; // 连接超时5秒 LHttp.ReadTimeout : 30000; // 读取超时30秒 LFileStream : TFileStream.Create(AFileName, fmCreate or fmShareDenyWrite); try LHttp.Get(AUrl, LFileStream); Result : True; finally LFileStream.Free; end; finally LHttp.Free; end; end;回调类型直接用Indy的TWorkEvent定义在IdComponent单元避免自定义回调签名和OnWork不匹配。ConnectTimeout和ReadTimeout必须显式设置尤其在内网带宽抖动时默认的无限超时会让升级程序挂在界面上用户只能强杀进程。另外fmShareDenyWrite这个打开模式很关键它在文件创建期间禁止其他进程写同一路径避免升级程序把自己刚下载的文件再写坏。如果发现下载后的文件和version.json里的size对不上优先怀疑是不是这里没有做共享限制。下载顺序也有讲究。多文件升级包在下载阶段就按顺序下载到临时目录全部下载成功后统一校验MD5任何一个文件校验失败就清空临时目录重来。这样可以避免主程序换了一半、本地文件新旧混杂的情况。临时文件建议存放到与主程序同卷的目录下比如{主程序目录}\UpdateTemp因为不同卷之间的文件移动是复制加删除跨盘移动失败率明显高于同卷重命名。3.3 升级器与主进程的握手协议下载校验完成后uMain.pas会把控制权交给独立的升级进程update.exe。这里有一个进程间的握手协议主程序把当前进程ID和更新参数传给升级器升级器在替换文件前先确认主程序已经退出。uses Winapi.ShellAPI, Winapi.Windows, System.SysUtils; procedure TfrmUpdate.LaunchUpdater; var LParams: string; LSei: TShellExecuteInfo; begin LParams : Format(/pid%d /src%s /dst%s, [GetCurrentProcessId, QuoteFileName(FTempPath), QuoteFileName(FAppPath)]); FillChar(LSei, SizeOf(LSei), 0); LSei.cbSize : SizeOf(LSei); LSei.fMask : SEE_MASK_NOCLOSEPROCESS; LSei.lpFile : PChar(ExtractFilePath(Application.ExeName) update.exe); LSei.lpParameters : PChar(LParams); if ShellExecuteEx(LSei) then WaitForSingleObject(LSei.hProcess, 60000); end;升级器拿到/pid参数后用OpenProcess轮询目标进程是否退出确认退出后再执行文件覆盖。这里用ShellExecuteEx而不是简单的ShellExecute是因为需要拿到进程句柄做超时等待主程序在60秒内没有退出就认为升级失败并唤醒用户处理。参数里的/src指向临时目录/dst指向主程序所在目录升级器按JSON里的files列表逐一移动文件。更稳的做法是升级器先把旧文件改名为.bak再从临时目录移入新文件。第二步失败时能把.bak恢复回去做到原地回滚。这块回滚逻辑在TestUpdate项目里体现得最明显下一章就围绕TestUpdate把它串起来。4. TestUpdate模拟客户端驱动的完整升级验证TestUpdate工程是一个最小可运行的主程序Main.pas里放着版本信息和模拟的业务界面。它的存在价值是让update工程在开发期就有对手戏可演主程序报出我是2.2.9版升级器检测到服务器是2.3.1版后完成替换。这个工程跑通了再往真实项目迁就只剩替换文件列表和版本常量。4.1 搭建本地升级验证环境TestUpdate和update两个工程用Delphi 10.4或11都可以直接打开编译Community Edition也足够跑这套demo。验证不需要内网服务器一台开发机就能完成。把TestUpdate.exe放到一个目录比如C:\AutoUpdateDemo\App把update.exe和version.json放到上一级目录然后启动一个静默的HTTP服务托管整个目录。Windows 10以上系统可以用PowerShell的简易服务器也可以直接用Pythoncd C:\AutoUpdateDemo python -m http.server 8080如果开发机没有Python用IIS或者HFS这类工具都行只要保证下面两个URL能访问到http://127.0.0.1:8080/version.jsonhttp://127.0.0.1:8080/packages/app/main.dllTestUpdate的Main.pas里把FServerUrl指向上面的地址FCurrentVersion保持2.2.9服务器端version.json的version写成2.3.1。此时触发轮询理想情况是TestUpdate界面上出现发现新版本2.3.1确认后下载临时文件关闭自身进程update.exe替换文件并重新拉起TestUpdate.exe。整个链路能走完自动化升级部分就算验证通过。4.2 版本比对分支与边界条件升级逻辑会遇到的版本关系不止服务器版本更高这一种。下面这张表列出需要覆盖的测试用例TestUpdate里每个case都有对应断言场景本地版本服务器versionminVersion预期行为正常升级2.2.92.3.12.0.0提示并升级版本一致2.3.12.3.12.0.0静默跳过服务器回滚2.3.12.2.92.0.0不处理等待下个周期强制升级1.9.82.3.12.0.0拦截不允许关闭窗口灰度未开始2.2.92.3.1(未来releaseTime)2.0.0不提示继续轮询版本回滚这个case容易写错。本地版本高于服务器版本时不降级是一个默认前提因为客户端卸载降级往往伴随数据兼容问题自动降级的风险大于收益。这意味着整个升级逻辑都是单向的如果服务器端误发布了一个低版本号客户端不会自动修正运维层面要留一个手动回滚手段而不是依赖客户端的自动行为。4.3 升级日志与失败回滚验证升级过程的每一步都要落日志否则没法排查用户说弹窗了但没升级成功这类问题。TestUpdate和update共用一个日志目录写文件采用追加方式保留最近5个文件。日志里至少要记录检测时间、服务器返回版本、下载文件数、校验结果、替换开始和结束时间。[2026-01-15 10:23:01] fetch version.json: version2.3.1, forcetrue [2026-01-15 10:23:02] compare local2.2.9 remote2.3.1, need upgrade [2026-01-15 10:23:05] download packages/app/main.dll, 154624 bytes [2026-01-15 10:23:06] md5 check pass, move to temp ok [2026-01-15 10:23:07] launch updater, pid8821 [2026-01-15 10:23:09] replace file ok, old backup kept as main.dll.bak回滚的验证方式是把待替换的main.dll故意写坏比如在文件尾部追加一个字节。升级器替换后启动TestUpdate如果启动闪退或版本号没变化检查是否存在同名的.bak文件存在就把.bak恢复回来并追加一条rollback日志。实际上这笔逻辑应该由升级器自己完成而不是靠人工所以这里要做的是确认升级器在文件替换失败时自动恢复了旧版本TestUpdate的下一次启动才会是旧版本号而不用重装。开发期遇到一个常见迷思升级程序本身不能升级自己。update.exe如果被业务文件更新覆盖下次升级时就没法拉起升级器。解决方法就是把update.exe独立于主程序目录或者至少保证任何升级包的files列表都不包含update.exe自身。TestUpdate这套结构里update.exe放在App上一级自然就避开了这个坑。5. 升级包签名校验与无感升级的落地细节5.1 在下载校验层加入签名验证MD5只能验证文件完整不能验证文件来源。升级服务器如果走公网最直接的办法是把version.json和升级包一起做签名。Delphi项目里加RS256签名需要引入第三方库对于内网工具类系统用共享密钥的HMAC就能防住大部分篡改场景。服务器端生成清单时额外发布一个sign字段内容是version.json主体文本按约定密钥计算的HMAC-SHA256值客户端先校验签名再解析字段。System.Hash单元提供了现成的HMAC实现uses System.Hash; function TUpdater.VerifySignature(const AJson, ASign, AKey: string): Boolean; begin Result : THashSHA2.GetHMAC(AJson, AKey) ASign; end;计算范围必须和服务器端完全一致要么对整个version.json文本要么对去除sign字段后的规范化文本两边约定好后用测试用例锁定。凡是签名校验失败的版本一律不进入下载流程并记录一条安全告警日志。更新频率低、带宽敏感的小团队可以跳过签名但最低限度要把下载URL限定为HTTPS。HTTPS证书链校验在Indy里默认开启不要因为内网证书过期就把TIdHTTP的证书校验选项设为绕过这个习惯带到外网项目会出问题。提示签名密钥不要写死在代码里。编译期常量会泄漏在exe的字符串中Delphi字符串裸眼可见。建议把密钥散列后分段放进注册表或配置文件中代码里只保存拼接规则。5.2 升级完成后由主程序接管启动升级器替换完文件后不直接启动新版本主程序而是把启动动作交还给主程序自己。实现上是在替换完成后创建一个标记文件如.updated主程序启动时发现这个标记就清理临时目录、删除.bak备份、输出一条升级完成日志。这样的好处是主程序可以决定在自身的数据迁移逻辑跑完后再清理旧文件避免升级器替主程序做决策。另一个实现细节是升级器的窗口隐藏。既然升级过程发生在主程序退出之后升级器的窗口就没有必要占用用户屏幕。设置Application.ShowMainForm : False同时定时检查主进程句柄是否退出。整个升级过程控制在几十秒内用户看到的是主程序短暂消失后带着新版本重新出现感知会舒服很多。需要强提醒时再通过TrayIcon在系统托盘给提示。往真实项目迁移时先跑通TestUpdate的case再把这几个常量替换成自己服务器的地址和版本清单路径链路就能复用。本文还有配套的精品资源点击获取