14 · 配置、平台抽象与更新链路¶
配置、平台和更新常被当作“外围代码”,但对长寿命终端控制平面而言,它们直接决定可恢复性:坏配置不能让所有 pane 消失,平台差异不能渗进每个 action,自更新更不能绕过 package manager 或杀掉活跃 Agent。
14.1 配置模型¶
Config 的顶层分区包括:
各区用 typed enum/struct 表达,例如 update channel、shell mode、新 terminal cwd 策略、sidebar、sound、theme、keybindings。默认值集中在 model/子模块,而不是散在 render/action 中。
14.2 为什么不是“解析失败就全用默认值”¶
用户可能只写错 [theme] 的一个颜色,而 [session] 的 history 设置和 [remote] 的 SSH 配置仍合法。全文件失败后静默用默认值,会让用户误以为设置生效且可能改变持久化行为。
LoadedConfig 同时返回:
- 合并后的
Config; - diagnostics;
- invalid sections。
这支持“合法分区继续生效,坏分区回落并明确报告”的部分容错。
14.3 Layering 与未知字段¶
加载逻辑位于 src/config/io.rs。可把合并过程看成:
flowchart LR
D["compiled defaults"] --> O["TOML overlay"]
O --> V["section validation"]
V --> K["keybinding normalization/conflict checks"]
K --> C["effective Config"]
V --> Diag["diagnostics + invalid sections"]
未知 key 需要诊断,因为它通常是拼写错误或用户参考了新版文档。直接 deny_unknown_fields 整体失败太脆,完全忽略又太隐蔽;Herdr 使用 section-aware overlay/diagnostic 在两者之间取平衡。
14.4 Reload 不是重启¶
server.reload_config 或 keybinding 触发 reload 后生成 ConfigReloadReport。配置字段按是否可热应用分类:
| 类型 | 例子 | Reload 行为 |
|---|---|---|
| 纯呈现 | theme、sidebar、tab bar | 更新 state,full render |
| 输入策略 | keybindings、right-click modifier | 重建索引/decoder policy |
| 新 runtime 默认 | shell mode、新 pane cwd | 影响未来创建,不强改现有 child |
| 持久化策略 | history/session 选项 | 更新后续 save behavior |
| 拓扑/实验能力 | 某些 server/remote 开关 | 可能要求 restart,报告而非假装已应用 |
Reload 的返回状态应区分 applied、partially applied、failed/restart required,而不是只返回 true。
14.5 Keybinding 是本地设备能力¶
远程 server 拥有动作语义,但用户按键来自本地 terminal。Client handshake 会携带规范化 keybinding profile/capability,避免远端配置错误解释本地组合键。
同时,全局动作和 terminal 字节要有明确 precedence:prefix/navigate mode 消费 Herdr binding;Terminal mode 中未匹配输入由 VT 编码。配置冲突检查要防止一个键同时被两个同层 action 无法预测地消费。
14.6 平台边界¶
src/platform/mod.rs 集中对 core 暴露的 OS 行为,具体实现在:
linux.rs;macos.rs;windows.rs;unix_common.rs;fallback.rs。
边界覆盖:
- foreground process/job 发现;
- signal/kill 与 detached daemon;
- shell/custom command builder;
- clipboard text/image、notification;
- terminal resize signal;
- runtime marker;
- 进程环境与路径;
- platform capabilities。
14.7 Capability 比散落 cfg 更有表达力¶
PlatformCapabilities 在快照中包括:
live_handoff:Unix true;direct_terminal_attach:Unix true;preserve_legacy_doubled_escape_input:macOS 特定兼容。
编译期 #[cfg] 仍用于平台实现,但上层产品决策读 capability:UI 是否显示 action、remote/update 选择什么计划、测试期望什么行为。这样不会在十几个业务 handler 里重复 cfg!(unix)。
14.8 进程发现为何必须平台化¶
Agent identity 依赖 foreground job:
- Linux 可读
/proc、process group/session; - macOS 使用不同 process APIs;
- Windows 有 process tree、console/ConPTY 和环境大小写规则;
- permission/race 可能让部分 argv 不可读。
共同返回 ForegroundJob { process_group_id, processes },每个 process 带 pid/name/argv0/argv/cmdline 的可选信息。Detector 只消费规范化模型,并对缺失字段降级,不直接散布 OS query。
14.9 Resize 与信号¶
Unix 注册 SIGWINCH handler时只设置 atomic flag,并使用 SA_RESTART,避免在 signal handler 中做不安全 I/O,也避免阻塞 read 因 EINTR 大量失败。主机 watcher 通过 take_terminal_resize_signal 消费 flag;Windows 依赖 size polling。
这是标准 signal-safe 模式:handler 只记录事实,普通控制流再执行复杂逻辑。
14.10 Updater 的数据来源¶
src/update.rs 使用 curl 子进程获取 manifest,没有引入完整 Rust HTTP client。两条 channel:
- stable:semver release、release notes、protocol、各平台 asset 与必须的 SHA-256;
- preview:base version + build id + commit + built_at + exact build assets/protocol。
src/build_info.rs 读取编译时 channel/build metadata。Windows 固定快照默认 preview,Unix 默认 stable。
使用 curl 的取舍:复用系统成熟 TLS/代理配置、减小 Rust 依赖;代价是运行环境必须有可用 curl,错误解析和进程调用要严格非交互化。
14.11 Update 选择不是“latest > current”这么简单¶
选择 release 时检查:
- 当前 channel;
- stable semver 或 preview build identity;
- 当前平台 asset 是否存在;
- manifest protocol;
- SHA-256;
- archived exact release/build metadata;
- 当前安装由谁管理;
- 是否有运行中的一个或多个 named session server。
Preview 不能只比较 semver,因为多个 preview 共享 base version;build id/commit 是真正 identity。
14.12 尊重 package manager 所有权¶
Updater检测当前 executable(跟随 symlink)是否属于:
- Homebrew Cellar;
- mise installs;
/nix/store。
若属于 package manager,自更新不会直接 rename 覆盖:
- Homebrew 提示
brew update && brew upgrade herdr; - mise 提示
mise upgrade herdr; - Nix 提示 profile/flake 更新;
- preview channel 对这些安装通常拒绝,要求 direct install。
这是文件所有权原则:能写一个路径不代表应该越过管理器写它,否则 receipts、rollback、GC 和签名链都会失真。
14.13 下载、校验和替换¶
Direct install 更新流程:
flowchart TD
Check["select release/asset"] --> Curl["curl to staging file"]
Curl --> Hash["verify SHA-256"]
Hash --> Perm["set executable permission"]
Perm --> Servers["plan running server actions"]
Servers --> Replace["rename staging over current exe"]
Replace --> Handoff["request compatible live handoff if selected"]
Handoff --> Confirm["probe new version/protocol"]
固定源码注释明确使用 rename 做 atomic replace。替换失败保留错误和 staging/旧可执行状态,不能留下半写 binary。
顺序中 server plan 与 binary replace 的细节会依据平台/选项调整,但不变量是:checksum 验证发生在执行新文件之前;运行 server 的处理是显式计划。
14.14 多 Session 更新计划¶
用户可能同时运行 default 和多个 named session,每个 server 的版本/capability不同。Updater 枚举 runtime status,为每个生成 RunningServerUpdatePlan:
- 是否需要 restart;
- protocol 是否兼容;
- 是否支持 Live Handoff;
- API socket/path;
- handoff 失败后的 recover/stop 选择。
herdr update --handoff 表示用户授权尝试无损接管,不表示可以对不支持 handoff 的 server 强行冒险。对太旧的平台,updater 会汇总并请求 stop 决策。
14.15 Handoff 失败后的二次判断¶
请求 Live Handoff 超时后,最危险的反应是立刻 kill old server。失败可能发生在:新 server未启动、已接管但 status probe 延迟、旧 server回滚成功、public socket 暂时切换。
更新代码会重新分类外部状态:
- 旧 server 仍运行且 panes 安全;
- 新 server 已运行;
- 无 server;
- 状态无法确定。
只有在明确情形和用户确认下才 stop。错误文案会指出旧 server 仍带着 panes 运行,保护用户不因一次升级失败损失工作。
14.16 配置/平台/更新的共同原则¶
这三部分看似无关,实际共享一个模式:
配置分区 owner、OS capability、package manager owner、server runtime owner 都是“谁有权改变这件事”的不同形式。
14.17 本章结论¶
- 配置 loader 应返回有效值与诊断,不让一个坏 section 吞掉整份配置;
- reload 要区分即时应用、只影响未来 runtime 和必须重启;
- 平台差异集中为规范化数据与 capability,上层按能力决策;
- updater 校验 channel/build/protocol/checksum,并尊重 package manager ownership;
- 更新运行中 server 是一组可恢复计划,handoff 失败后必须重新探测而非盲目 stop。
下一章聚焦这些架构为何能在几十个 pane、多个客户端和高频 PTY 输出下仍工作:并发所有权、队列边界、合并信号、retained rendering 和每个慢消费者的隔离。