【免费下载链接】Netcatty
SSH workspace, SFTP, and terminals in one
本文围绕 Netcatty 开源仓库中针对 GitHub issue #3213(大文件传输缓慢/暂停续传失效/强杀后无法恢复)与 issue #3155(海量文件下界面冻结)的专项研究文档展开,深入讲解全局传输调度器队列合并、传输中心弹窗视口虚拟化、以及断点续传时远程源文件前缀的有序 SHA-256 校验这三处核心缺陷的根因与修复方案,并给出对应的回归测试与可选真机验证方法。读者读完可以掌握:如何在保留优先级、所有者公平性与每主机并发上限的前提下消除入队 O(n) 扫描;如何让包含数万行任务记录的传输中心只挂载视口内行;以及为什么"连续确认区间才是安全检查点",以及如何把验证延迟从秒级降到毫秒级。
背景:两个 issue 的共性症状与本次修复的边界
issues-3213-3155-sftp-responsiveness.md 是 2026-08-31 针对提交39d7c38a6进行的专项调查记录。其中:
- #3213报告大文件传输缓慢/不稳定、暂停与继续失效,以及杀掉应用后无法恢复;
- #3155报告文件数量很大时整个界面冻结。
调查指出,两份报告都没有提供复现方向、服务器配置或传输日志,因此文档中给出的结论是可复现的代码缺陷,而非"所有报告的症状都是同一个原因"的断言,两个 issue 均保留待上报者确认。研究结论聚焦在三个确定可复现的缺陷上:
globalTransferScheduler.run在每次插入时扫描整个等待队列(见 globalTransferScheduler.ts);- 传输中心弹窗(transfer-center popover)无条件挂载每一个顶层任务行,即使这些行远在视口之下(见 GlobalSftpTransferCenter.tsx);
- 远程源文件/前缀验证使用 ssh2 的串行
createReadStream,而正文下载早已使用流水线(pipelined)读取(见 transferBridge.cjs)。
同时文档明确划定了正确性边界(Correctness boundaries retained),这些约束在后续源码中都有对应体现:
- 只有连续确认(contiguous acknowledged)的区间才作为检查点(checkpoint);聚合展示进度与稀疏文件长度都不是安全偏移量;
- 首次运行强杀恢复时,若没有捕获到完整的源身份(source identity),仍从零开始;自上次生命周期保存以来的进度仍可能丢失——本次 PR 改善的是已验证续传路径的延迟,而不是所有整应用强杀场景;
- SCP 与旧的
fastPut路径不会获得不支持的暂停/续传能力; - 源文件变更、暂存前缀不匹配、缺少暂存、取消、替换、权限与冲突处理都保留原有检查;
- 不对任意服务器断电持久性、所有 Windows 服务器、或通用的吞吐量倍增作任何承诺。
缺陷一:入队即全队列扫描的调度器
问题本质:插入触发 O(n) 扫描
旧版globalTransferScheduler.run在每次任务插入时都会遍历整个等待队列去做准入检查。文档给出了一个可复现的数据点:
在 2 个活动任务之后入队 10,000 个任务,本地耗时 1,237 ms,进行了 49,985,005 次 limit 检查。
也就是说,随着队列规模增长,入队本身变成了二次方复杂度,这是"文件一多界面就卡"的调度侧根源之一。
修复方案:合并队列泵(coalesced queue pump)
在 globalTransferScheduler.ts 中可以看到当前实现:
schedulePump()使用pumpScheduled标志把一次发现/入队突发合并成一次扫描:队列非空且没有已排定的泵时,通过queueMicrotask(或达到 64 次启动阈值后改用setTimeout(run, 0))安排一次pump();pump()每轮从队列中选出"可运行(canRun)且优先级最高,或在优先级相同时让位于上次未占用资源的所有者"的候选任务,执行后立即释放资源槽并再次schedulePump();- 每累计 64 次任务启动就主动让出事件循环,保证立即完成的小任务批处理也不会饿死用户输入/绘制。
文档给出的对照结果:合并队列泵后,同样场景从 49,985,005 次检查降到 10,001 次(本地运行 3-6 ms),而优先级、所有者公平性与每主机并发上限保持不变。测试 globalTransferScheduler.test.ts 覆盖了这些不变量:
scheduler limits each remote host independently:每主机独立并发限制;scheduler alternates owners when both have queued work:所有者轮换公平性(["a1", "b1", "a2"]);prioritize moves a queued transfer ahead of fairness ordering:优先级可越过公平顺序;queued work stays paused until resumed and can be cancelled:暂停/取消/恢复语义;enqueuing a large blocked batch does not repeatedly inspect the existing queue:阻塞批入队时应线性检查(readsWhileBlocked <= count * 3);large batches of immediately completed files yield to user input:大批立即完成的任务必须让位于输入;a synchronous job failure releases its slot for queued work:同步失败释放槽位。
调度器还通过getSftpTransferResourceKeys(globalTransferScheduler.ts)把每个任务映射到host:<id>/session:<id>/local资源键,配合 transferLimits.cjs 中的并发窗口实现"按资源限流、按所有者轮换"的准入模型。
缺陷二:传输中心无条件挂载全部任务行
问题本质:每行都是真实 DOM 节点
传输中心弹窗过去会为每一个顶层任务行都渲染真实 DOM,即使这些行远在可视区域之下。文档给出的实测数据:
实际 Electron 组件显示 1,000 行大约耗时 1,885 ms。
当历史任务达到数万行(文档中验证场景是 20,000 个任务入队)时,这种全量挂载必然导致滚动与打开弹窗的卡顿,这正是 #3155 "文件很多时冻结" 的 UI 侧根源之一。
修复方案:有界视口虚拟化
修复后弹窗改为测量到的、有界的视口挂载(bounded viewport mounting),大约只挂载 9 行。关键设计约束包括:
- 所有任务仍然保留在 store 中,滚动与桶(bucket)选择依然能暴露它们——虚拟化只是渲染层的裁剪,不改变数据层;
- 文件夹展开状态被放到被回收行之外持有,避免回收复用导致展开状态丢失。
对应的 DOM 测试 GlobalSftpTransferCenter.virtualization.test.tsx 验证了:
- 注入 1,000 个顶层任务后,渲染出的
role=progressbar行数在(0, 40)区间内,而不是 1,000; - 通过滚动到最底部可以渲染并定位到
file-999.bin(最后一个文件); - 虚拟行即使被 viewport wrapper 包裹,也不得丢失其分隔线(
last:border-b-0:last-child断言); - 测试环境还模拟了 460px 高的滚动容器,验证真实滚动边界。
配套的 GlobalSftpTransferCenter.resume-status.test.tsx 与 GlobalSftpTransferCenter.accessibility.test.ts 分别覆盖续传状态渲染与无障碍语义,保证虚拟化没有破坏任务状态的可读性。
缺陷三:续传源前缀验证从串行流改为有序流水线哈希
问题本质:续传验证一次网络往返读一个块
暂停时 Netcatty 会捕获一个完整的源身份(identity),续传前必须先验证远端源前缀与暂停时一致,才能继续写文件。旧实现走 ssh2 的串行createReadStream(client.sftp.createReadStream),一个 READ 一个往返。文档给出的实测数据:
一个 128 MiB 回环 SFTP 测试(READ 回复被延迟),旧代码在 resume 阶段耗时 41,493 ms。
也就是说,暂停本身是瞬时的(几毫秒),而"继续"要等几十秒做验证,界面表现为"暂停/继续失效"。
修复方案:复用下载侧的有序 64 请求 × 32 KiB 窗口
正文下载早已使用流水线读取(DOWNLOAD_TRANSFER_CONCURRENCY = 64、TRANSFER_CHUNK_SIZE = 32 * 1024,见 transferLimits.cjs)。本次修复让验证路径复用同一套有序全区间哈希辅助函数:hashRemotePrefixWithSftpRanges(transferBridge.cjs)按窗口顺序哈希,使峰值驻留缓冲区保持在并发扇出范围内(约 2 MB),而不是整个多 GB 前缀。
其核心特性在源码中一一对应:
- 完整校验每一个必要字节:
resumeContentVerifyBytes(transferBridge.cjs)返回"保存的完整前缀必须仍然匹配"所需的字节数——因为暂停确认可能先于指纹捕获发生,那个窗口内源文件被改写,绝不能让旧的暂存字节与新指纹的后缀混合(见hashRemotePrefix上方的注释块,transferBridge.cjs); - 有界内存:按 64 请求窗口分批读入
Buffer,窗口内通过Promise.race对每窗口的"不活动看门狗"(inactivity watchdog)与读完成竞争; - 取消与不活动截止:
createSharedAbortGate与SFTP_REQUEST_TIMEOUT_MS(30 秒)配合,超时抛SFTP_READ_TIMEOUT并标记sftpRequestTimedOut,随后abandonWedgedVerificationSftpChannel放弃卡住的通道; - 保留串行兼容路径:当服务器拒绝并发 READ(返回普通协议错误)时,代码保留
createReadStream串行回退路径,但不会在请求超时时回退——超时通道在被再次尝试前就被放弃(对应文档 Review follow-up 的第一条); - 短 READ 也算活动:
onRead回调每次 READ 落地都会重新武装看门狗,且窗口结束(windowActive = false)后不会发出下一次部分 READ 或迟到地重新武装看门狗(对应文档第二条); - 不降级为元数据/采样/文件大小校验:文档明确"这并不能替代元数据、采样或文件大小对内容的验证"——
validateTransferResumeSource仍检查 size/lastModified(dedicatedTransferResume.ts),但内容层面必须做全量前缀哈希。
为什么保留"更严格的校验":参考成熟客户端的取舍
文档的成熟客户端对比解释了这些设计决策:
- OpenSSH同样是 32 KiB 请求 + 64 请求默认窗口,中断时停止新请求、排空回复,并单独跟踪连续确认前缀与最高确认位置;其手册警告不匹配的部分内容会损坏续传文件。Netcatty 保留了更强的内容检查(全量前缀哈希)。
- FileZilla(SVN r11556)分批消费目录结果并把续接投递到 UI 循环,其队列在总量/方向/站点限制下填充可用传输槽。Netcatty 借鉴了"协作式工作与有界准入",而不是无限制的
Promise.all或更大的包。 - WinSCP默认两个后台操作、续传使用临时文件再发布;rclone 文档同样记录 32 KiB/64 请求默认值、服务器兼容性顾虑,以及检查/传输竞争受限连接池时可能死锁。这些都不构成"两个文件就是 Netcatty 瓶颈"或"任何被杀的进程都能安全续传"的结论;未来自适应窗口或服务器兼容矩阵应作为独立的、可测量的工作另行开展。
回归测试与可选真机验证
纯单元/组件回归
本次修复的每个缺陷都先观察到失败再修复,对应测试:
- 调度器准入/顺序/让出:
enqueuing a large blocked batch does not repeatedly inspect the existing queue、large batches of immediately completed files yield to user input等(globalTransferScheduler.test.ts); - 完整远程前缀验证:
hashRemotePrefix/hashRemotePrefixWithSftpRanges相关单元与集成测试(transferBridge.test.cjs); - 传输中心真实 DOM 边界与滚动:
large transfer histories mount only visible rows and can scroll to the last file(GlobalSftpTransferCenter.virtualization.test.tsx)。
可选的 loopback 真机 SFTP fixture
文档提供了一个可选加入(opt-in)的真机 SSH/SFTP fixture 脚本 sftp-transfer-resume.live.test.cjs,它只监听 loopback、使用生成的测试凭据、隔离全部临时/home 状态。它创建保存的前缀、检查续传、(对足够长的传输)执行实时暂停/继续,最后校验完整输出的 SHA-256。它不是整应用强杀场景的模拟。
NETCATTY_SFTP_LIVE=1 SFTP_LIVE_MIB=128 SFTP_LIVE_FILES=12 \ node scripts/sftp-transfer-resume.live.test.cjs NETCATTY_SFTP_LIVE=1 SFTP_LIVE_MIB=128 \ SFTP_LIVE_BASELINE_REF=39d7c38a6 \ node scripts/sftp-transfer-resume.live.test.cjs第二条命令通过git show加载基线提交39d7c38a6的electron/bridges/transferBridge.cjs(见脚本 sftp-transfer-resume.live.test.cjs),用于对比修复前后的验证延迟。脚本内的 fixture 服务端(sftp-transfer-resume.live.test.cjs)对每个 READ 回复注入SFTP_LIVE_DELAY_MS(默认 5 ms)延迟并统计并发峰值,从而把"慢服务器"下的流水线收益量化出来。
文档记录的实测观察(属于 fixture 特定观察,不是性能承诺):
- 12 个 128 MiB 文件两两提交全部完成且哈希一致;
- 每个文件都经历了暂停/继续:暂停确认(pause acknowledgement)耗时 6-15 ms,续传(resume)耗时约 1.45-3.18 秒;
- Electron 验证还覆盖了生产弹窗、暂停全部/继续全部、滚动到最后一个文件、桶切换,以及弹窗打开状态下 20,000 个任务入队。
fixture 同样遵守"隔离"约束:测试在托管临时目录内分配专属子目录并隔离 staging/home 状态(sftp-transfer-resume.live.test.cjs),清理只删除该唯一子目录,即使 fixture 建立失败也一样——对应文档 Review follow-up 的第三条。
Review follow-up:保留的正确性与被否决的方案
文档最后列出了评审后的后续事项,其中几项在源码中有直接对应:
- 串行前缀验证保留:当服务器以普通协议错误拒绝 range OPEN/READ 时,保留串行
createReadStream路径;取消与请求超时不回退,超时通道先被放弃(transferBridge.cjs)。 - 短 READ 视为看门狗活动:每个正向短 READ 都计入不活动看门狗,但不改变有序全区间哈希;已结束的窗口不能发布迟到进度、重新武装看门狗或再发一次部分 READ(transferBridge.cjs)。
- 分隔线基于真实列表位置:传输行分隔线依据实际列表位置而非临时 viewport wrapper 计算,最后一个列表行不能因 wrapper 而丢失分隔线——对应虚拟化测试中的
last:border-b-0:last-child断言。 - 否决周期性全量历史保存:曾提议的周期性全量历史保存被移除——20,000 个排队文件时,仅序列化就每 5 秒占用约 80 ms;紧凑日志(compact journal)还需要跨窗口所有权与重试尝试协调,这些恢复语义不属于本次聚焦响应性的 PR。没有新增持久化定时器、存储键或 schema 迁移。
对应地,store 侧 sftpTransferCenterStore.ts 的持久化逻辑(PERSIST_INTERVAL_MS = 250、emitProgress纯进度路径不触碰localStorage、按动画帧合并进度通知)同样贯彻了"进度洪泛不得冻结 UI"的原则——大目录历史在同步存储上每几秒 JSON.stringify 会冻结 CSS 动画,因此进度 tick 只唤醒进度订阅者,生命周期/结构变化才立即持久化(见emit/emitProgress的实现,sftpTransferCenterStore.ts)。
小结
这次针对 #3213/#3155 的专项修复,本质上是把三处"规模放大就退化"的实现换成有界、协作、可让出的方案:调度器把入队扫描从每次插入合并为每批一次、传输中心把全量挂载换成有界视口虚拟化、续传验证把串行流换成与下载一致的有序 64×32 KiB 流水线哈希。与此同时,连续确认区间才作检查点、全量前缀哈希不可降级、强杀恢复的持久化语义不变这些正确性边界被完整保留——修复提升的是已验证续传路径的延迟与大数据量下的交互响应,而不是靠牺牲内容校验或放松恢复语义换来的。相关实现与测试均可直接在 application/state/sftp/、components/GlobalSftpTransferCenter.tsx、electron/bridges/transferBridge.cjs 与 scripts/sftp-transfer-resume.live.test.cjs 中继续深入阅读。
【免费下载链接】Netcatty
SSH workspace, SFTP, and terminals in one
相关推荐
Netcatty SFTP 传输审计(2026-09):暂停/恢复竞态修复与断点续传可靠性验证
Netcatty SFTP 传输审计(2026 09):暂停/恢复竞态修复与断点续传可靠性验证 2026 年 9 月,Netcatty 仓库完成了一次范围明确(
Netcatty SFTP 目录传输结算观察机制:同 ID 所有权交接下的"文件夹永不完成"问题与修复
Netcatty SFTP 目录传输结算观察机制:同 ID 所有权交接下的"文件夹永不完成"问题与修复 本篇技术指南围绕 Netcatty(SSH 工作区、SF
opencodex Responses API Item ID 前缀校验硬化:四个 400/404 根因与防御纵深修复实践
opencodex Responses API Item ID 前缀校验硬化:四个 400/404 根因与防御纵深修复实践 在 opencodex 代理(Uni
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考