尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenLogi IPC 线缆协议剖析:tarpc + bincode 的位置化、只追加线缆格式与版本管理

OpenLogi IPC 线缆协议剖析:tarpc + bincode 的位置化、只追加线缆格式与版本管理 OpenLogi IPC 线缆协议剖析tarpc bincode 的位置化、只追加线缆格式与版本管理【免费下载链接】OpenLogi⚡️A native, local-first alternative to Logitech Options, written in Rust — remap buttons, DPI, and SmartShift over HID. No account, no telemetry.项目地址: https://gitcode.com/GitHub_Trending/op/OpenLogiOpenLogi 的桌面 GUI 与后台 agent 是两个独立进程它们通过一个本地 socket 上的 tarpc bincode 线缆协议对话。本文以crates/openlogi-ipc为核心完整解读这份 append-only只追加 线缆格式的设计约束为什么方法顺序与枚举变体索引就是线缆格式本身、为什么protocol_version必须永远是方法 0、黄金字节测试如何钉死每一处线缆布局以及 debug 构建的 agent 为什么永远不会夺走正在运行的 release agent。读完本文你将掌握 OpenLogi IPC 的架构骨架、各 RPC 方法语义、版本升级规范以及如何在修改线缆类型时正确地跑通验证流程。一、架构总览GUI 与 agent 如何对话1.1 传输层interprocess 本地 socketOpenLogi 的 agent服务端与 GUI客户端通过 tarpc 在interprocess本地 socket 上进行 RPC 通信线缆定义位于 crates/openlogi-ipc/src/ipc.rsUnix文件系统 Unix-domain socket位于openlogi_core::paths::agent_socket_path()生产构建默认~/.config/openlogi/agent.sockmacOS 本地-devbundle 使用兄弟的openlogi-dev配置目录避免开发 agent 占用已安装应用的端点。WindowsOS 命名空间下的命名管道\\.\pipe\openlogi-agent.sock。interprocess在 Unix 与 Windows 上暴露同一套 API因此 agentbind与 GUIconnect共用一条代码路径两端都通过wrap()用 长度前缀 bincode 的同一帧格式包裹连接。bind()的try_overwrite(true)会清理非干净退出SIGKILL / panicabort / 掉电残留的 Unix socket 文件否则监听器Drop未执行时残留 socket 会一直导致AddrInUse。相关实现见 crates/openlogi-ipc/src/transport.rs。1.2 为什么是位置化线缆格式线缆格式是位置化positional的原因有两层bincode 编码枚举的变体索引variant index而非#[repr(u8)]判别值——这两者可能不一致tarpc 编码的是 trait 方法的方法顺序method ordertarpc 会从 trait 生成一个请求枚举bincode 编码该枚举的变体索引。因此Agenttrait 的方法声明顺序、serde 枚举的变体声明顺序都直接构成了线缆字节的一部分。这是该 crate 最核心的纪律任何穿过 IPC 边界的类型都是只追加的append-only永远不允许重排或删除。1.3 线缆边界比本 crate 更宽线缆表面wire surface不只是本 crate 内的类型。来自openlogi-core的 serde 类型设备模型、DeviceKind、动作、配置与openlogi-hid的写错误WriteError会骑在 RPC 负载里穿越边界因此同样的只追加规则同样约束它们。也就是说改openlogi-core或openlogi-hid中任何跨边界 serde 类型与改本 crate 的线缆类型一样敏感。二、协议版本protocol_version必须永远是方法 02.1 严格相等无兼容协商PROTOCOL_VERSION当前为 29见 crates/openlogi-ipc/src/ipc.rs它独立于 crate 版本号仅在线缆类型发生破坏性变更时递增。GUI 在连接时通过Agent::protocol_version检查该值严格相等strict-equal时才继续驱动不匹配则直接拒绝transient onlyGUI 与 agent 打包在同一个.app中并原子更新。线缆注释中明确说明没有小版本 / 兼容协商。因为 GUI 与 agent 在同一 bundle 中发布agent 二进制被替换时会 re-exec 自己所以严格相等 干净拒绝就是完整契约。方法顺序是线缆格式的一部分若protocol_version不再是第一个方法握手本身在版本偏移时就会解码失败此时连检测并报告不匹配都做不到了。2.2 连接握手client.rs 的统一机制所有客户端——设置应用与 overlay 助手——都必须走同一条路径连接、包裹流、spawn tarpc client、在发起任何真实 RPC 之前读取 agent 的协议版本。不同的只是不匹配时的策略应用告知用户哪一侧过期overlay 让出自己的角色因此机制集中在 crates/openlogi-ipc/src/client.rspub async fn connect() - ResultConnection, ConnectError { let stream transport::connect().await?; let client AgentClient::new(client::Config::default(), transport::wrap(stream)).spawn(); let version client.protocol_version(context::current()).await?; Ok(Connection { client, version }) }版本故意不在本函数内校验——调用方对比PROTOCOL_VERSION后按方向决策更老的 agent 等着被替换更新的 agent 说明本进程才是过期方。ConnectError区分两种截然不同的失败Endpointsocket 不可达agent 未运行、未监听、端点名无法解析与Handshakesocket 接受了连接但 agent 从未应答握手——挂起或垂死的 agent而非缺席。2.3 实践中如何使用CLI 的优雅降级openlogi-cli 的 list 命令 展示了版本策略的落地先以 2 秒超时尝试client::connect()若版本不匹配打印提示note: the agent speaks protocol v{}, this CLI expects v{PROTOCOL_VERSION} — reading hardware directly随后绕过 agent 直接读硬件。若连接成功则以ClientKind::Cli声明身份见下文第 4.5 节使休眠 agent 无需武装整套输入栈即可服务一次查询。三、黄金字节测试钉死线缆格式3.1 测试文件与运行方式线缆格式的每一处布局都由 golden test 钉死位于 crates/openlogi-ipc/tests/wire_format.rs。运行方式cargo test -p openlogi-ipc --test wire_format规则是任何触碰线缆类型的提交在 push 之前都必须跑这个测试。失败信息会打印新编码的字节left是新编码。3.2 序列化选项DefaultOptions不是自由函数线缆使用 tokio-serde 的Bincode::default()即 bincode 1.3 的DefaultOptionsvarint 整数、little-endian、拒绝尾部多余字节。而自由的bincode::serialize/deserialize函数使用fixint编码不会产生匹配的字节。测试辅助函数因此显式走bincode::DefaultOptions::new().serialize(...)而不是自由函数。3.3 黄金测试的验证范围每个assert_wire都做双向验证序列化字节与黄金值严格相等再反序列化回原值并重新序列化确认往返一致。黄金测试覆盖了请求变体顺序方法顺序即线缆ProtocolVersion {}00、SetDpi04、NextPairing0d、Snapshot0e、PollEventMonitor0f、Identity16、Observe17、ObserveActionRing18、DeclareClient19等protocol_version_is_pinnedassert_eq!(PROTOCOL_VERSION, 29)任何黄金值重生成都必须伴随版本号提升同一 diff 中可见agent 身份冻结Identity两半都是裸u64黄金0712永不变化——新旧任何构建都能解码helper 才能读懂让它离开的指令各类 DTOAgentStatus、AgentSnapshot、ForegroundApps、配对阶段、事件监视器事件、Actions Ring 类型、设备库存、设备设置负载、独立灯光设备等。测试文件中特别钉死了几个看似无辜的修复陷阱例如 serde 编码SmartShiftMode的变体索引Free0, Ratchet1而非#[repr(u8)]固件判别值1/2FeatureUnsupported变体索引对 GUI 停止重复探测是承重load-bearing的不仅关乎可解码性。四、RPC 服务契约Agent trait 逐方法解读#[tarpc::service] pub trait Agent定义了完整的方法面。以下是按声明顺序的关键方法及其语义全部见 crates/openlogi-ipc/src/ipc.rs方法语义protocol_version() - u32握手用方法 0跨所有版本线缆稳定status() - AgentStatusAccessibility / hook / 自启动状态供 GUI 门禁与设置页inventory() - VecDeviceInventory最新设备库存快照GUI 在窗口打开时按定时器轮询reload_config() - Result(), ConfigReloadError重读config.toml并重建绑定/DPI 映射GUI 保存配置后调用set_dpi/read_dpi立即应用 / 读取 DPI 值滑动条预览与提交set_lighting/set_smartshift/read_smartshift立即应用灯光与完整 SmartShift 配置request_accessibility_prompt()由 agent 发起 Accessibility 弹窗使系统对话框指向真正被信任的 agent 二进制start_pairing/pair_device/cancel_pairing配对会话的三种命令agent 独占设备 I/Onext_pairing() - OptionPairingUpdate长轮询下一步配对事件已被状态化PairingPhase取代方法保留只因方法顺序即线缆snapshot() - AgentSnapshot原子获取状态 最新库存v4 追加poll_event_monitor() - VecMonitorEvent排空 hook 观察到的事件v9 追加set_light/set_light_manual_power独立灯光命令与相机联动灯的手动功率覆盖next_action_ring() - OptionActionRingInvocation长轮询下一次 Actions Ring 调用已被状态化取代方法保留action_ring_hover/action_ring_activate/action_ring_cancel环交互三命令带ActionRingCommandErroridentity() - Identity本 agent运行实例的身份冻结位置永不变observe(since: Generation) - Observation阻塞到可观察状态与since不同然后整体返回observe_action_ring(since: Generation) - RingObservation环的独立状态通道契约同observedeclare_client(kind: ClientKind)声明连接类型对休眠 agent 是承重信息v29 追加4.1 observe边沿驱动的时间、全量状态的内容tarpc 是严格的请求/响应没有服务端推送。因此 agent 需要告诉客户端的事被塑造成客户端保持打开、agent 一直持有直到有话说或持有窗口耗尽的请求。Agent::observe就是这条状态通道时间上是边沿驱动agent 的任一 watcher 改变状态时立即应答内容上永远是完整当前状态绝不是增量客户端从任意起点新窗口、重连、agent 重启都能收敛——用最后一次见过的 generation或0问一次即可。这让协议免除了订阅、回放与缺口检测两侧都不需要记忆任何历史。持有窗口OBSERVE_HOLD 20 秒无可报告时 agent 在窗口耗尽后以不变状态应答这同时充当存活心跳——挂起而非死亡的 agent 停止应答死亡的 agent 则掉落 socket。该常量在 crates/openlogi-ipc/src/ipc.rs 导出客户端设置请求 deadline 必须高于它因为 tarpc 会取消 deadline 过期的 handler。Generation单调递增仅在状态真正变化时递增。0是客户端I 什么都没见过的哨兵agent 的 cell 从 1 开始因此第一次observe(0)立即应答而不是等 20 秒的持有期去等一个已经发生的变化。4.2 服务端实现ObservableStateagent 侧的状态 cell 在 crates/openlogi-agent-core/src/observable.rs内部是tokio::sync::watch通道update()通过send_if_modified只在真正有差异时递增 generation 并通知——一个不改变任何东西的写入不通知任何人这正是读者可以阻塞在该 cell 上而非定时重采样的原因。该 cell 有多个写者orchestrator 写设备与配置事实、agent 二进制写 hook 事实因此以Arc共享每个 setter 都取self。单元测试覆盖了关键语义重复枚举不唤醒读者a_repeated_enumeration_notifies_nobody、权限撤销与 hook 退役在同一 generation 完成a_revoke_retires_the_hook_in_the_same_generation、持有期耗尽返回不变状态nothing_to_report_answers_with_the_unchanged_state、静默写入不终止持有期a_silent_write_does_not_end_the_hold。4.3 PairingPhase配对会话是状态不是事件流AgentSnapshot::pairingv20 追加把配对会话建模为状态而非步骤流Searching/Found(VecFoundDevice)/Pairing/Passkey(PasskeyMethod)/Paired { slot }/Failed(PairingFailure)。这让 agent 中途重启自愈替代 agent 没有会话因此停留在 Searching… 的窗口自行消解无需为其合成终止事件。终态Paired、Failed一直保留到会话被取消或新会话开始结果不会像事件那样落在两次观察之间。旧的next_pairing事件流仍被忠实服务只因方法顺序是线缆格式。PairingFailure是类型化错误Hid、ReceiverNotFound、Register、Timeout、Device { code }、Cancelled、ReceiverBusy、WatcherUnavailable、AgentRestarted、ReceiverAccessUnavailable、AlreadyActive、UnknownDevice、NoActiveSession跨 agent↔GUI 边界保持类型化GUI 可以据此选择恢复 UI、遥测与本地化文案而不必匹配人类可读字符串。PairingCommandError只覆盖agent 无法接受命令本身的失败接受后的进度仍通过状态呈现。4.4 RingObservation 与 Actions Ring 呈现observe_action_ring在独立的 cell上运行而不是AgentSnapshot的字段overlay 是工作集完全不同的观察者为了每次设备热插拔都唤醒它并塞给它完整设备库存对一个只负责按键按下的瞬间画出环的 helper 是错误的。RingObservation携带 generation 与OptionActionRingInvocation——None即没有环要显示关闭也因此到达无需发明closed消息。ActionRingInvocation只携带只读呈现快照标签、literal标志用户自写文案原样渲染避免与本地化键碰撞v16、已解析图标与语言。可执行动作永不跨入 overlay helper留在 agent 拥有的会话中。overlay 的完整实现见 crates/openlogi-overlay/src/agent.rs连接、声明ClientKind::Overlay、读取identity并与succession的 allegiance 比对superseded 则让位退出终端命令activate/cancel会重试直到会话自身 deadline而同一环的更新命令会取代supersede停滞命令而非排队保证环的响应性。4.5 declare_clientmacOS 休眠门禁的承重信息ClientKind有三个变体Gui桌面应用唯一能武装休眠 agent 的类型、Cli读取快照不武装、Overlayoverlay helper不武装——自行连接的是上一次运行的孤儿。连接握手后立即声明。AgentServer把每次连接的声明转发到休眠门禁dormancy gate的 demand 通道见 crates/openlogi-agent/src/server.rs。接管探测takeover probe从不声明——它只讲protocol_version——因此也永远不会武装休眠 agent。五、接管Takeover如何用只读握手替换旧 agent5.1 背景二进制 watcher 覆盖不到的旧 agent二进制 watcher 只存在于带有它的二进制里因此第一次协议版本提升仍会搁浅每个运行着更新前 agent 的用户旧 agent 从不退出、launchd 只在退出时行动、它持有单例锁使每个新 spawn 的 agent 失败退出、新 GUI 又拒绝旧协议——用户被钉在连接界面直到下次登录。接管逻辑见 crates/openlogi-agent/src/takeover.rs。5.2 接管流程新 agent 失去单例锁后以客户端身份连接 IPC socket向锁持有者询问协议版本握手protocol_version握手跨版本线缆稳定方法 0裸u32所以对任何过去版本的 agent 都有效。等待窗口HANDSHAKE_TIMEOUT 2 秒答不上来的持有者视为无法推理的卡死状态不动它。版本比较持有者版本更老→ 是更新前的遗留物终止它并夺取锁持有者版本相同或更新→ 我们才是重复或过期的那一个照旧退出。终止手段SIGTERM 而非礼貌 RPC——过去版本的协议没有 quit 方法。太老而无法处理信号的持有者死于信号在 launchd 下这是非成功退出于是 launchd 以 bundle 路径即新二进制重生它随后的锁竞争败者干净退出新到能处理 SIGTERM 的持有者释放事件 tap 并以 0 退出launchd 不干预锁落到我们手中。无论哪条路最终恰好一个最新 agent 存活。锁重试LOCK_RETRY 20 × 200 ms 预算覆盖慢速退出。5.3 debug 构建永不接管设计而非缺陷try_replace_stale()在cfg!(debug_assertions)下直接返回None日志记录 debug build — leaving the running agent in place。debug 构建的 agent 永远不会夺走正在运行的 release agent——这是有意的设计开发 agent 不得挤掉用户的生产 agent不是 bug。这是 AGENTS.md 明确强调的约束之一。Windows 侧没有历史包袱从未发布或自启动过 agentbinary_watch在更新时退出、GUI 的 spawn 重试启动新二进制因此replace_stale()直接返回None。六、升级线缆类型的完整规程综合 AGENTS.md 与源码任何线缆变更必须遵循只追加服务方法只在末尾追加永不重排或删除跨边界的 serde 枚举只追加变体永不重排。若看起来需要重排说明设计上应新增类型/方法而非修改旧布局。提升版本任何线缆变更都提升PROTOCOL_VERSION并同步更新protocol_version_is_pinned测试中的断言当前为 29。重新生成黄金值更新 crates/openlogi-ipc/tests/wire_format.rs 中的 golden hex。若失败是有意为之从断言消息left是新编码取实际十六进制替换黄金值。跑测试cargo test -p openlogi-ipc --test wire_format——任何触碰线缆类型的 push 之前都必须运行。尊重冻结面identity()方法的位置与Identity两半u64的类型是冻结的——新旧任何构建都要能解码关于 agent 的新事实放新方法或AgentStatus绝不放进 identity。保持客户端 deadline调用observe/observe_action_ring时把 RPC deadline 设在OBSERVE_HOLD20 秒之上否则 tarpc 会在持有期结束前取消 handler。七、设计与演进参考docs/DECISIONS.md的架构决策记录把这条线缆称为项目的两条生产基础设施之一另一条是succession的单进程-单角色生命周期模式并记录了agent 保持单进程跨越边界的边获得一条线缆而非事件层的决策依据任何候选切分点都穿过热路径CGEventTap hook 与 HID 设备 I/O 是同一个输入循环的两端GUI/agent 拆分之所以成立正是因其跨越边是冷路径配置保存、快照、长轮询。其既定方针是任何将来跨越进程边界的边都应获得openlogi-ipc契约上的版本化方法只追加、黄金测试、PROTOCOL_VERSION提升外加一个succession角色管理新进程的生命周期——但第一动作永远是一条线缆而不是一个抽象。这套设计让 GUI、agent、CLI 与 overlay 四个进程共享同一个版本化契约GUI 是纯 IPC 客户端lib.rs 说明本 crate 是叶子 crate只依赖openlogi-coreGUI 拉入线缆契约无需链接openlogi-hid/hidpp/async-hidagent 侧回答这些 RPC 的运行时hook 运行时、设备 I/O、Actions Ring 会话状态留在openlogi-agent-core反向依赖本 crate。理解这份线缆格式就理解了 OpenLogi 多进程架构的全部边界约束。【免费下载链接】OpenLogi⚡️A native, local-first alternative to Logitech Options, written in Rust — remap buttons, DPI, and SmartShift over HID. No account, no telemetry.项目地址: https://gitcode.com/GitHub_Trending/op/OpenLogi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表