OpenLogi Three-Process Architecture Communication: IPC Design and Implementation
OpenLogi uses a three-process architecture where the GUI, Agent, and Overlay communicate exclusively via a tarpc/bincode local socket, eliminating hardware access from the UI layer while maintaining responsive, push-based state synchronization.
The AprilNEA/OpenLogi repository implements a local-first replacement for Logitech Options+ by decoupling device I/O from the user interface. This architecture relies on three specialized processes that coordinate through a strictly versioned IPC contract defined in the openlogi-ipc crate.
The Three-Process Architecture
OpenLogi splits functionality across three isolated processes to enforce security boundaries and platform-specific responsibilities.
GUI Process (openlogi-desktop)
The GUI process (crates/openlogi-desktop) renders the Settings UI, per-app profile picker, and Actions Ring overlay using the GPUI framework. Crucially, this process never touches hardware directly—all device interactions are delegated to the Agent via the IPC socket.
Agent Process (openlogi-agent)
The Agent process (crates/openlogi-agent) is a headless background server that owns all mouse and keyboard HID++ device I/O, platform input hooks (macOS/Windows), and the Unix domain socket listener. It acts as the single source of truth for device state and executes all hardware commands.
Overlay Process (openlogi-overlay)
The Overlay process (crates/openlogi-overlay) is a lightweight helper that draws the cursor-centered Actions Ring. It links only against openlogi-ui (containing presentation data like ring geometry and icons) and communicates with the Agent using the same IPC contract as the GUI, allowing it to operate independently without pulling in heavy GUI dependencies.
The IPC Contract and Wire Protocol
All inter-process communication flows through a tarpc/bincode local socket defined in openlogi-ipc/src/ipc.rs. The contract centers on a pub trait Agent service definition that both client and server implement.
Protocol Versioning and Stability
The IPC protocol maintains strict backward compatibility through two mechanisms:
- Method ordering: New RPC methods must be appended to the trait to preserve bincode variant indices (see the comment at lines 18-22 in
openlogi-ipc/src/ipc.rs). Reordering existing methods breaks the wire format. - Protocol version constant: The
PROTOCOL_VERSIONconstant (line 61) is checked during the initial handshake; a mismatch forces the GUI to relaunch and upgrade.
Core RPC Methods
The Agent trait exposes read-only queries and fire-and-forget writes:
- State observation:
protocol_version(),status(),inventory(),snapshot(), andobserve()provide poll-free access to the complete Agent state. - Device control:
set_dpi(),set_lighting(),set_smartshift(), andset_light()returnResult<(), WriteError>after performing HID++ operations. - Pairing workflows:
start_pairing(),pair_device(),cancel_pairing(), andnext_pairing()manage device discovery. - Actions Ring:
next_action_ring(),observe_action_ring(),action_ring_hover(), andaction_ring_activate()handle the overlay interactions.
How the GUI Communicates with the Agent
The GUI spawns an IPC client thread in crates/openlogi-desktop/src/services/ipc.rs (function spawn() at lines 31-36) that manages the connection lifecycle.
Connection Lifecycle
- Runtime creation: The client creates a dedicated Tokio runtime on an OS thread (lines 39-45).
- Handshake: It connects via
openlogi_ipc::client::connect()and validates the remotePROTOCOL_VERSION. If the Agent is newer, the GUI emitsGuiUpdate::OutdatedGui(lines 68-71). - Long-polling: The client maintains one persistent
observerequest at all times (lines 65-70). The Agent returns a fullAgentSnapshotonly when observable state changes (new devices, DPI adjustments, etc.), eliminating polling timers. - Command channel: UI actions send commands through an
mpsc::UnboundedSender<Command>(lines 27-31). The client loop handles these viahandle()and forwards them as RPC calls.
Resilience and Auto-Spawning
If the socket disappears, the client enters a retry loop using RECONNECT_DELAY and SPAWN_RETRY_PERIOD. If the Agent is not running, the GUI can spawn a fresh binary via spawn_agent() (lines 75-86).
Server Implementation in the Agent
The AgentServer implementation in crates/openlogi-agent/src/server.rs (lines 90-138) holds the runtime state:
orchestrator: Core state machine managing device configuration and DPI cycles.observable: AnArc<ObservableState>storing the complete GUI-visible state (status, inventory, foreground application).- Subsystem helpers:
pairing,event_monitor,action_ring, anddispatchermanage specific responsibilities.
Read-only RPC methods simply read from observable (e.g., status(), inventory()). Write methods route through shared.device(&route).run(...) to execute HID++ operations and return typed WriteError results.
The observe(since) method (line 22 of the trait) implements the state channel using a monotonically increasing generation counter. When state changes, the Agent immediately responds to pending observe calls with a fresh Observation containing the new generation and AgentSnapshot.
Practical Implementation Examples
Spawning the IPC Client
use openlogi_desktop::services::ipc::spawn;
// Initialize inside the GPUI app:
let ipc = spawn(); // Returns IpcClient { updates, commands }
let updates_rx = ipc.updates; // Receive GuiUpdate events from Agent
let cmds_tx = ipc.commands; // Send Command::SetDpi, etc.
See the full implementation in crates/openlogi-desktop/src/services/ipc.rs (lines 31-36).
Requesting a One-Off Snapshot
use openlogi_ipc::client::connect;
// In an async context:
let conn = openlogi_ipc::client::connect().await?;
let snapshot = conn.client.snapshot(context::current()).await?;
println!("Agent version: {}", snapshot.status.agent_version);
The snapshot RPC is defined in crates/openlogi-ipc/src/ipc.rs (line 118).
Changing Device DPI
use openlogi_core::hid::{DeviceRoute, Dpi};
let route = DeviceRoute::new(/*receiver*/ 0, /*device*/ 1, /*endpoint*/ 0);
let new_dpi = Dpi::new(1200);
// Fire-and-forget via the command channel
let _ = cmds_tx.send(Command::SetDpi(route, new_dpi));
Commands are handled in crates/openlogi-desktop/src/services/ipc.rs (lines 95-97) and forwarded to the Agent's set_dpi method in crates/openlogi-agent/src/server.rs (lines 34-42).
Observing State Changes
// Inside the client's observe_loop:
let observation = client.observe(context::current(), last_generation).await?;
let generation = observation.generation;
let snapshot = observation.snapshot;
// Update UI, then immediately re-poll
last_generation = generation;
Implemented in the client at crates/openlogi-desktop/src/services/ipc.rs (lines 71-78) and the server at crates/openlogi-agent/src/server.rs (lines 22-24).
Summary
- OpenLogi isolates hardware access in the Agent process, UI rendering in the GUI process, and overlay drawing in the Overlay process.
- All communication uses a tarpc/bincode local socket defined by the
Agenttrait inopenlogi-ipc/src/ipc.rs. - The protocol uses append-only method definitions and a
PROTOCOL_VERSIONconstant to ensure wire compatibility. - The GUI maintains a long-poll
observeconnection for push-based state updates, eliminating polling overhead. - The system is resilient: the GUI auto-spawns the Agent if missing, and the Agent holds a single-instance lock to prevent duplicates.
Frequently Asked Questions
How do the OpenLogi processes communicate with each other?
The three processes communicate exclusively via a local Unix socket using the tarpc framework with bincode serialization. The contract is defined by the pub trait Agent in openlogi-ipc/src/ipc.rs, which exposes methods for state observation (observe, snapshot) and device control (set_dpi, set_lighting). This socket-based approach ensures the GUI never requires hardware permissions, while the Agent runs with elevated privileges to access HID++ devices.
What happens if the Agent process crashes while the GUI is running?
The GUI's IPC client (crates/openlogi-desktop/src/services/ipc.rs) detects socket disconnection and enters a retry loop with configurable RECONNECT_DELAY and SPAWN_RETRY_PERIOD. If the Agent is not found, the GUI automatically spawns a new Agent binary via spawn_agent() (lines 75-86). Additionally, the Agent itself holds a single-instance lock (agent.lock) to prevent duplicate processes, and implements takeover::try_replace_stale() to restart if a stale version is detected.
Why must new RPC methods be appended to the trait definition?
The IPC uses bincode for serialization, which assigns variant indices to enum and trait methods based on their declaration order. Adding methods in the middle or reordering existing ones shifts these indices, breaking wire compatibility with running processes. The comment at lines 18-22 in openlogi-ipc/src/ipc.rs explicitly warns that new methods must be appended to maintain backward compatibility. Breaking changes require bumping the PROTOCOL_VERSION constant (line 61) to force coordinated upgrades.
How does the Overlay process render the Actions Ring without the GUI?
The Overlay links only against the openlogi-ui crate, which contains shared presentation data including ring geometry, icons, and locale strings. It communicates directly with the Agent using the same IPC contract methods (observe_action_ring, action_ring_activate) as the GUI. Because it inherits its own TCC identity (accessibility permissions) via launch_agent() in the IPC client (lines 15-19), it operates independently without sharing the GUI's accessibility grant or heavy rendering code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →