跳转至

14 · 配置、平台抽象与更新链路

配置、平台和更新常被当作“外围代码”,但对长寿命终端控制平面而言,它们直接决定可恢复性:坏配置不能让所有 pane 消失,平台差异不能渗进每个 action,自更新更不能绕过 package manager 或杀掉活跃 Agent。

14.1 配置模型

Config 的顶层分区包括:

theme, terminal, session, update, keys, ui,
worktrees, advanced, experimental, remote

各区用 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 行为,具体实现在:

边界覆盖:

  • 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/capability/version
  -> 再选择允许的动作
  -> 动作前验证
  -> 动作后重新 probe
  -> 无法证明状态时停止破坏性推进

配置分区 owner、OS capability、package manager owner、server runtime owner 都是“谁有权改变这件事”的不同形式。

14.17 本章结论

  1. 配置 loader 应返回有效值与诊断,不让一个坏 section 吞掉整份配置;
  2. reload 要区分即时应用、只影响未来 runtime 和必须重启;
  3. 平台差异集中为规范化数据与 capability,上层按能力决策;
  4. updater 校验 channel/build/protocol/checksum,并尊重 package manager ownership;
  5. 更新运行中 server 是一组可恢复计划,handoff 失败后必须重新探测而非盲目 stop。

下一章聚焦这些架构为何能在几十个 pane、多个客户端和高频 PTY 输出下仍工作:并发所有权、队列边界、合并信号、retained rendering 和每个慢消费者的隔离。