Skip to content

第 4 章:Torrent 生命周期 —— 从一段元数据到可恢复任务

.torrent 文件不是任务,magnet 链接也不是任务。只有当元数据、用户覆盖、resume 状态、文件事实和 Session 策略合并后,tr_torrent 才成为一个真正可运行的领域对象。

一、创建前先有 tr_ctor

Transmission 使用 tr_ctor 收集创建参数。它同时容纳:

  • 来源:本地 .torrent、原始 bencode、magnet link;
  • 强制值:本次添加明确指定的目录、暂停状态、Peer 限制;
  • fallback:来自 Session 的默认下载目录、默认暂停状态、默认 Peer 限制;
  • 文件选择与优先级;
  • 是否删除原始 torrent 文件;
  • 验证完成回调。

这相当于 Builder,但它还承担“值从哪里来”的优先级语义。TR_FORCETR_FALLBACK 使 RPC/UI 的本次参数可以覆盖全局默认,又不会直接修改 Session。

二、添加流程的七个阶段

2.1 身份以 info hash 为准

tr_torrentNew() 先从 ctor “偷走”解析后的 metainfo。没有 info hash 就拒绝;Session 的 tr_torrents 容器已有相同 hash 就报告重复。临时 torrent id 在 daemon 重启后可能改变,而 info hash 是稳定身份。

2.2 注册发生得很早

tr_torrent::init() 在读取 resume 之前就调用 session->addTorrent(this):分配 id、加入 queue、为 Peer Manager 建立 swarm。之后 resume 加载与信号连接都能通过稳定 id 找到任务。

2.3 配置合并有明确顺序

tr_resume::load() 的核心顺序是:

text
ctor 强制字段 → resume 文件 → ctor fallback 字段

已加载的字段用 64 位 bit mask 记录,后续来源只补缺失项。这样“这次明确要求暂停”不会被旧 resume 覆盖,“没有明确要求下载目录”则能恢复上次目录。

三、.torrent 与 magnet 走的是两段式生命周期

3.1 完整 torrent

tr_torrent_metainfo::parse_benc() 解析 announce list、文件、piece hash、piece length、私有标志、webseed 等信息,并计算 info dictionary 的 hash。任务一开始就能建立:

  • tr_block_info:总大小、piece/block 数量与坐标转换;
  • tr_file_piece_map:文件、piece、字节范围关系;
  • tr_completion:已有 block/piece;
  • 文件 wanted 与 priority 状态。

3.2 magnet

magnet 初始只有 info hash、显示名、tracker/webseed 等少量信息,不能知道文件布局和 piece hashes。Torrent 可以绕过普通下载队列先启动,与支持 LTEP 的 Peer 交换 ut_metadata

收到 metadata piece 后,tr_metadata_download 检查编号与长度、拼接 info dictionary,并做两级验证:

  1. 计算完整 metadata 的 SHA-1,必须等于 magnet 的 info hash;
  2. 把它包装成合成 torrent 顶层字典,再走标准 bencode metainfo 解析。

成功后保存 .torrent、删除 .magnet,调用 set_metainfo() 重建 completion/file map 等结构,再进入与普通 torrent 相同的校验和启动流程。

设计重点

magnet 没有另写一套长期下载逻辑。它只是一种“元数据尚未补齐”的临时状态,一旦补齐就汇入标准 tr_torrent_metainfo 路径。

四、Torrent 状态不是一个 enum

公开活动状态 TR_STATUS_* 是多个内部字段的派生结果:

text
Verify Active  → CHECK
Verify Queued  → CHECK_WAIT
is_running     → DOWNLOAD 或 SEED(取决于 completion)
is_queued      → DOWNLOAD_WAIT 或 SEED_WAIT
其他           → STOPPED

这比维护一个可随意赋值的大 enum 更安全:验证、运行、队列和完成度各自有独立事实,activity() 只负责组合展示。

五、启动为何分成两段

tr_torrent::start() 可从任意调用线程进入,先在锁内做快速判定:

  • 已在下载/做种就直接返回;
  • 正在验证则等待验证结束;
  • 队列没有空槽则只设置 is_queued_
  • 本地文件消失则设置 local error;
  • 手动重启已达 ratio 的任务时,关闭 torrent 级 ratio 限制。

随后设置运行意图,并把 start_in_session_thread() 投递到 Session 线程。第二段才做有副作用的系统动作:

  1. 为 wanted 文件创建空文件;
  2. 重算完成度;
  3. 清除 queued;
  4. 重置本次运行统计与错误;
  5. 让 Announcer 发送 STARTED;
  6. 安排 LPD;
  7. 发出 started_ 信号。

这种两段式把“调用者立即看到的意图”与“必须在事件线程串行完成的动作”分开。

六、停止、删除与删除数据是三件事

6.1 停止

tr_torrentStop() 清除 start_when_stable_,然后在 Session 线程运行 stop_now()。后者停止验证、发信号、向 Tracker 安排 STOPPED、关闭任务文件句柄并保存 resume。

6.2 从 Session 删除任务

tr_torrentRemove() 标记 is_deleting_ 后排队执行。删除任务会:停止 torrent、从 Announcer/Peer Manager/容器/队列移除、删除 .torrent/.magnet/.resume 状态文件,最后释放对象。

6.3 删除本地数据

只有 delete_flag 为真时才调用 tor->files().remove(...)。执行前先关闭文件句柄并从验证队列移除,防止 Windows 文件占用或后台校验与删除竞争。文件删除失败会记录警告,但仍继续移除 torrent 元对象。

这三个语义必须在 UI/RPC 层清楚区分,否则“移除任务”很容易变成不可恢复的数据删除。

七、一个 block 完成后发生什么

tr_torrent::on_block_received() 是下载数据闭环的关键:

  1. 重复 block 只修正下载计数,不重复写完成状态;
  2. tr_completion 中标记 block;
  3. 计算该 block 覆盖的首尾 piece;
  4. 对刚刚凑齐的 piece 执行 check_piece()
  5. 成功调用 on_piece_completed(),失败调用 on_piece_failed()

Piece 成功后会发信号、标记需要重算 completeness,并检查它是否让某些文件完整。文件完成时关闭句柄、记录 mtime、把 .part 路径改回正式文件名。

Piece 失败则增加 corrupt 计数、回退 downloaded 计数、清除已有 piece,并通知 Peer Manager。Peer Manager 可借助每个 Peer 的 blame/strike 统计识别持续提供坏数据的节点。

八、完成度为什么还要重算

“所有 wanted 文件完成”和“整个 torrent 所有 piece 完成”不是同一件事。用户可以取消选择文件,但边界 piece 可能同时覆盖 wanted 与 unwanted 文件。tr_completiontr_file_piece_map 共同判断:

  • TR_LEECH:仍缺 wanted 数据;
  • TR_PARTIAL_SEED:wanted 文件已完成,但 torrent 并不完整;
  • TR_SEED:拥有全部数据。

这一状态会影响 queue direction、Tracker 上报、Peer 策略、完成脚本和文件移动。

九、恢复文件是任务状态的快照,不是事实来源

.resume 保存上传/下载计数、Peer、进度、wanted、优先级、限速、目录、运行意图、日期、ratio/idle、文件名、标签、分组和顺序下载设置。

但磁盘可能在 Transmission 关闭期间被修改。恢复进度时会结合文件 mtime 与 checked-pieces bitfield,决定哪些 piece 仍可信。完整验证永远以文件内容和 metainfo hash 为准。

这是重要原则:resume 是加速恢复的缓存,磁盘字节与 cryptographic hash 才是真相。

十、异步回调为什么常捕获 id 而不只捕获指针

验证、移动和 RPC 异步动作完成时,用户可能已经删除 torrent。安全模式是捕获 session + tor_id,回到 Session 线程后再次查询 session->torrents().get(id),并确认仍是同一对象、未处于 deleting 状态。

只捕获裸 tr_torrent* 再延迟执行,会把生命周期问题变成 use-after-free。源码中值得重点审查所有“lambda 捕获 tor 指针并跨事件循环”的位置。

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。