那阵子在收拾七亿批量远程管理工具(RdpManager)的 FTP 文件窗口。起因是有人反馈:传一个里面全是小文件的文件夹,软件会卡死,卡到只能去任务管理器杀进程。

我先试着复现。FTP 窗口里选一个文件夹点上传,队列一下就冒出好几千行——原来是它把文件夹里每个文件都平铺成了一条队列项。小文件一多,界面线程就在反复重建那张表格,越建越卡,最后整个窗口不响应。SFTP 那边同样的操作一点事没有,因为它传文件夹时,队列从头到尾只显示"文件夹"一项。

连着还有两个毛病。一个是上传大文件时进度条一直停在 0,字节其实在传,是刷新的时候没重画进度列;另一个是选文件夹的同时又加了别的任务,会跟正在跑的那条传输抢同一条 FTP 控制连接,直接挂死——查远端同名它是逐个文件去 GetObjectInfo 的,那几条请求没走 _opLock。

改法就是让 FTP 对齐 SFTP 那套:文件夹上传/下载当成一个任务,内部自己递归,进度按总字节算,队列里只留一行;查远端同名改成把目标目录整列一次来比对,不再逐文件往返;所有 _ftp 操作都收进 _opLock 里串行。

真正让我后背发凉的是改到"取消"这一支的时候。

我当时在翻上传/下载被取消之后,收尾要清理什么。FTP 那版代码里写着 File.Delete(t.Local)。第一眼没觉得有问题——直觉是"取消嘛,把没传完的那半截删掉"。可 t.Local 在上传任务里,指的是本地那个源文件。也就是说,用户传一个本地文件到服务器,中途点了取消,这段清理会把用户自己机器上的原始文件删掉。

我怀疑自己看错了字段,又回去把上传链路从头捋了一遍:AddUp 里 t.Local 确实存的是本地路径,t.Remote 才是远端。删远端倒还说得过去,传了一半的远端残文件该清;删本地是另一码事。这段清理的出发点大概是"下载单文件失败或取消时,把本地没下完的那半截删掉",逻辑本身没问题,问题是这个判断没分方向,上传也被一并算进去了。

改起来就一行:只有"下载单文件"取消或失败才删本地半截,上传的取消、失败,绝不碰本地。Local 和 Remote 这两个名字,方向错一次就是删人家源文件;这种地方,以后我都会多看一眼。

回头看,这个 bug 当时并没真咬到人——至少我没收到过"文件被删了"的反馈。可它是活的,只要有人在 FTP 上传到一半点取消,代码就走到那句删除。这种"没人报错、但确实会出事"的,比已经炸了的更难发现,只能靠改代码时顺手盯。

那轮改动落在 FtpForm.cs 里,方法动了不少:AddUp 重写、AddDown 重写、RunAsync 拆开,新加了 UploadTreeAsync、DownloadTreeAsync、EnsureRemoteDirsAsync、ScanTreeAsync。全围着"文件夹当一条任务"转。FTP 这个窗口在七亿批量远程管理工具里算是多灾多难的,前头刚修完一把锁(FTP 删完文件整个窗口卡死那次),也是刷新偷偷 await 了同一把 _opLock。两回撞的都是一个毛病:活儿早干完了,卡在后面那步收尾。

想拿带这版修复的包,官网下载页一直挂在那儿:七亿批量远程管理工具下载。

顺带提一句,七亿批量远程管理工具是免费的,Windows 端,RDP 九宫格、SSH 终端、SFTP、FTP 都在里头。