第 12 章:Daemon 与多端前端 —— 同一个内核的五种控制方式
Transmission 没有强迫所有平台使用同一 UI 技术。它统一的是内核与控制语义,展示层则保持原生。理解每个前端“在哪里运行 Session、怎样拿状态”比比较控件更重要。
一、产品拓扑
二、Daemon:外层也有一条事件循环
tr_daemon::start() 创建自己的 libevent base,用于 OS signal 与状态输出;然后 tr_sessionInit() 创建内核 Session——后者又有自己的专属 event base/thread。
Daemon 启动顺序:
- 合并内置 defaults、settings.json 与 CLI 参数;
- 建立 signal 处理(POSIX signalfd/平台等价);
- 创建 Session 并注册 RPC callback;
- 立即保存修正后的 settings;
- 可选创建 watch directory;
- 用 ctor 加载保存的 torrents;
- 运行 daemon 外层 event loop;
- 收到 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。
用户点击 → 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:轮询模型 + 虚拟列表
浏览器启动时:
session_get获取 daemon 配置;torrent_get初始化 torrent 模型;- 周期获取 active/recently active torrents;
- 对详情窗口按需请求 files/peers/trackers;
- mutation 使用无 id notification 或普通 request;
- 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/macOS | Qt | Web/remote |
|---|---|---|---|
| Session 位置 | 当前进程 | 当前进程或远端 | 远端 daemon |
| 控制协议 | C API | 统一 RpcClient | HTTP JSON-RPC |
| 更新方式 | callback + UI idle | Future + polling/signal | polling |
| 本地文件操作 | 完整 | 仅 local 模式 | 无 |
| 网络断线语义 | 不适用 | 显式 network state | 重试/告警 |
十二、架构启示
- 共享业务内核,不强求共享 UI。 原生体验与核心一致性可以同时拥有。
- 远程能力是一级产品边界。 RPC 不是 daemon 外挂,而是多个前端共同依赖的稳定协议。
- 本地模式也可复用远程语义。 Qt 证明这能显著降低分支,但要保留 locality 概念。
- 前端刷新是独立性能问题。 table payload、recently_active、虚拟列表分别优化网络、增量和 DOM。
源码锚点
daemon/daemon.cc:启动、reload、watchdir 与关闭gtk/Application.cc、gtk/Session.cc:本地 Session 与 Glib 回调桥qt/RpcClient.cc、qt/Session.cc:本地/远端统一web/src/remote.js、web/src/transmission.js:轮询与模型更新utils/remote.cc:命令行 RPC 客户端cli/cli.cc:deprecated 单 torrent 客户端