Skip to content

第 12 章:Daemon 与多端前端 —— 同一个内核的五种控制方式

Transmission 没有强迫所有平台使用同一 UI 技术。它统一的是内核与控制语义,展示层则保持原生。理解每个前端“在哪里运行 Session、怎样拿状态”比比较控件更重要。

一、产品拓扑

二、Daemon:外层也有一条事件循环

tr_daemon::start() 创建自己的 libevent base,用于 OS signal 与状态输出;然后 tr_sessionInit() 创建内核 Session——后者又有自己的专属 event base/thread。

Daemon 启动顺序:

  1. 合并内置 defaults、settings.json 与 CLI 参数;
  2. 建立 signal 处理(POSIX signalfd/平台等价);
  3. 创建 Session 并注册 RPC callback;
  4. 立即保存修正后的 settings;
  5. 可选创建 watch directory;
  6. 用 ctor 加载保存的 torrents;
  7. 运行 daemon 外层 event loop;
  8. 收到 stop 后销毁 watchdir/timer、保存设置、关闭 Session。

外层 loop 不处理 Peer 网络,它只让进程管理事件与 Session 内核解耦。

三、SIGHUP 如何热加载

POSIX daemon 收到 SIGHUP 后不会重启:

  • 重新打开日志文件,配合 logrotate;
  • 从配置目录重读 settings;
  • tr_sessionSet() 应用新配置;
  • reload blocklists;
  • 向 systemd 更新状态。

如果 Session 尚未完全启动,先记 seen_hup_,稍后执行。这个小状态避免启动竞态导致 reload 丢失。

并非所有设置都适合无损热切换;tr_session::setSettings() 内部负责比较旧值并重启/重绑必要子系统。

四、Watch directory 是一个输入适配器

Daemon 可监控目录,发现 .torrent 后走正常 ctor/add 流程。不同平台选择 inotify、kqueue、Win32 或 generic polling;handler 只关心“稳定可读的新文件”。

它没有绕过重复检测、默认目录、暂停策略和 metainfo 校验,因此自动添加与 RPC 添加不会形成两套业务规则。

五、GTK:直接链接内核,跨回调回到 Glib

GTK Application 创建本地 tr_session*Session wrapper 直接调用 tr_torrent* 和 C API。内核通过 tr_sessionSetRPCCallback() 通知 torrent add/remove/change;回调再用 Glib::signal_idle() 切回 GTK 主线程更新 model。

text
用户点击 → GTK handler → C API → Session线程
Session状态变化 → RPC callback → Glib idle → GTK model/view

这里的 “RPC callback” 是内核事件枚举回调,不代表一定经过 HTTP。它被本地 GUI 用来观察与 RPC 相同的领域变化。

关闭时 tr_sessionClose() 是阻塞操作,因此 GTK 放到辅助线程,再通过 idle signal 回主线程完成窗口退出,避免 UI 假死。

六、Qt:把本地和远端都抽象成 RPC

Qt 的 RpcClient 有两个 start():传 tr_session* 是本地模式,传 QUrl 是远端模式。

  • 本地:构造 tr_variant 请求,直接调用 tr_rpc_request_exec()
  • 远端:用 QNetworkAccessManager 发 JSON-RPC,处理 session id、认证和网络错误;
  • 两者都返回 QFuture<RpcResponse>,上层 Session 不需要为大多数动作分叉。

这是本仓库最强的解耦示例:即使内核在同一进程,也主动走 RPC 语义,保证本地/远端 UI 功能一致。

Qt 仍保留 locality 判断,因为“打开下载文件夹”“本地文件选择器”“watch dir 路径”在远端没有相同语义。抽象不能抹掉真正的物理边界。

七、macOS:原生对象包装 C 核心

Cocoa 前端用 Objective-C++ .mm 文件直接持有并调用 libtransmission。Controller、Torrent wrapper 与多个 ViewController 把 KVO/通知/窗口行为映射到 C API。

它与 GTK 同属 in-process 产品,但生命周期由 NSApplication/Cocoa run loop 驱动。平台专属能力(Quick Look、Dock badge、Sparkle、Power Management、Bonjour)停留在 macOS 层,没有进入核心。

八、Web UI:轮询模型 + 虚拟列表

浏览器启动时:

  1. session_get 获取 daemon 配置;
  2. torrent_get 初始化 torrent 模型;
  3. 周期获取 active/recently active torrents;
  4. 对详情窗口按需请求 files/peers/trackers;
  5. mutation 使用无 id notification 或普通 request;
  6. 409 时缓存新 session header 并重试。

torrent_get format=table 降低 payload;Clusterize 只渲染可见列表行,解决数千 torrents 的 DOM 成本。前端模型按 id 合并变化,并处理 removed 集合。

Web 没有 WebSocket 推送,状态新鲜度取决于轮询间隔。这换来更简单的服务端与断线恢复。

九、transmission-remote:CLI 是 RPC payload 编译器

utils/remote.cc 把大量命令行选项聚合成一个或多个 JSON-RPC 请求,用 libcurl 发送。它支持:

  • 主机/端口/路径与 Unix socket;
  • Basic auth、.netrc、TLS 校验开关;
  • 409 自动重试;
  • debug 打印请求响应;
  • JSON 原样输出或人类格式化表格;
  • batch 组合与超时推导。

它不链接下载会话去“远程控制”;它链接的核心类型主要用于 variant、quark、格式化与共享协议常量。

十、旧 transmission-cli 为什么仍存在

它是一次只处理一个 torrent 的早期本地客户端,直接创建 Session。官方 README 已标记 deprecated,推荐 transmission-remote。保留它主要服务老硬件/脚本兼容,不代表架构推荐。

成熟项目常同时存在“推荐路径”和“兼容路径”。源码导读应明确区分,否则容易从旧入口推导出错误的整体设计。

十一、前端共性与差异

维度GTK/macOSQtWeb/remote
Session 位置当前进程当前进程或远端远端 daemon
控制协议C API统一 RpcClientHTTP JSON-RPC
更新方式callback + UI idleFuture + polling/signalpolling
本地文件操作完整仅 local 模式
网络断线语义不适用显式 network state重试/告警

十二、架构启示

  1. 共享业务内核,不强求共享 UI。 原生体验与核心一致性可以同时拥有。
  2. 远程能力是一级产品边界。 RPC 不是 daemon 外挂,而是多个前端共同依赖的稳定协议。
  3. 本地模式也可复用远程语义。 Qt 证明这能显著降低分支,但要保留 locality 概念。
  4. 前端刷新是独立性能问题。 table payload、recently_active、虚拟列表分别优化网络、增量和 DOM。

源码锚点

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