How OpenLogi Architecture Works: GUI and Agent Process Communication
OpenLogi implements a strict client-server architecture where the GUI runs as a pure IPC client that communicates with a background agent process via tarpc, while the agent exclusively owns hardware access and maintains observable state through long-polling RPC calls.
OpenLogi is an open-source device management application that separates user interface concerns from hardware operations through a well-defined inter-process communication (IPC) layer. Understanding how the OpenLogi architecture coordinates its GUI and agent processes reveals a robust design pattern for desktop applications requiring privileged hardware access. The system distributes work across three distinct tiers—openlogi-desktop, openlogi-agent, and shared core libraries—to ensure security and stability, as documented in the repository's AGENTS.md file.
OpenLogi Architecture Overview
The codebase ships three processes bundled together but operating with distinct responsibilities:
- GUI Process (
openlogi-desktop+openlogi-overlay): Acts as a pure IPC client that renders the UI, receives user input, and forwards device commands. It never touches hardware directly. - Agent Process (
openlogi-agent): Runs as a background server that owns the input hook, performs all HID++ device I/O, and maintains the observable state. - Shared Orchestration (
openlogi-agent-core,openlogi-ipc): Provides the IPC contract and runtime utilities used by both sides, ensuring type safety across process boundaries.
IPC Contract and Wire Protocol
The GUI and Agent communicate through a strict request/response RPC defined by the Agent trait in openlogi-ipc/src/ipc.rs. This contract uses tarpc to generate client and server glue, with bincode handling serialization.
The wire format stores the variant index of enum methods, making method order part of the binary protocol. As noted in lines 41-45 of ipc.rs, methods must only be appended to maintain backward compatibility. The first method, protocol_version, performs handshake validation against PROTOCOL_VERSION (currently 29) to ensure GUI and Agent compatibility.
// Excerpt from openlogi-ipc/src/ipc.rs
#[tarpc::service]
pub trait Agent {
async fn protocol_version() -> u16;
async fn set_dpi(route: DeviceRoute, dpi: Dpi) -> Result<(), WriteError>;
async fn observe(since: Generation) -> Observation;
// ... additional methods
}
Agent Process Implementation
The agent implements the Agent trait in openlogi-agent/src/server.rs, serving as the single source of truth for device state. It holds the input hook, HID++ backends, and device enumeration caches.
Key responsibilities include:
- Observable State Management: Exposes
AgentStatus,AgentSnapshot, andObservationtypes that represent the complete system state. - Long-Polling RPC: Provides the
observe(since: Generation)method that blocks until state changes or theOBSERVE_HOLDtimeout (20 seconds) expires. This level-triggered approach allows the GUI to reconnect without missing updates. - Hardware Commands: Handles mutations like
set_dpi,set_lighting, andstart_pairingthrough the HID++ protocol layer.
// Agent-side implementation excerpt from server.rs
async fn set_dpi(
ctx: Context,
route: DeviceRoute,
dpi: Dpi,
) -> Result<(), WriteError> {
// The agent owns the HID backend; perform the write here.
agent_core::write::set_dpi(route, dpi).await
}
GUI Client Implementation
The desktop GUI creates an IpcClient in openlogi-desktop/src/services/ipc.rs that manages connection lifecycle and state synchronization.
Connection and Handshake
The ensure() function (lines 54-87) opens the local socket, checks PROTOCOL_VERSION, and declares the client kind as ClientKind::Gui. If the agent socket is absent, the SpawnReflex utility decides when to launch the agent based on SPAWN_AFTER_LOSS and SPAWN_RETRY_PERIOD timers, preventing rapid respawn loops while ensuring recovery from crashes.
Observation Loop
The observe_loop() maintains exactly one in-flight observe request at all times (lines 78-84). When a new Observation arrives containing a higher generation number, the GUI receives a GuiUpdate::Snapshot with the full AgentSnapshot. If the hold expires without state changes, the same snapshot serves as a heartbeat.
// GUI-side IPC client creation
use openlogi_desktop::services::ipc::IpcClient;
let ipc = IpcClient::spawn();
std::thread::spawn(move || {
while let Some(update) = ipc.updates.recv() {
match update {
GuiUpdate::Snapshot(snapshot) => {
ui::update_from_snapshot(snapshot);
}
GuiUpdate::Unreachable => ui::show_error("Agent not reachable"),
GuiUpdate::OutdatedGui => ui::show_error("Please restart OpenLogi"),
_ => {}
}
}
});
Command Handling
UI actions (DPI adjustments, lighting changes, pairing initiation) are sent through an mpsc channel to the IPC thread as Command enum variants. The thread re-uses the existing tarpc connection, ensuring that pending observe requests remain undisturbed. Errors translate into UI-visible GuiUpdate variants like LightCommandResult or PairingUndeliverable.
// Sending a DPI command from the UI
use openlogi_core::hid::{DeviceRoute, Dpi};
let route = DeviceRoute::new(/* receiver, device id */);
let dpi = Dpi::new(800);
let _ = ipc.commands.send(Command::SetDpi(route, dpi));
State Flow and Lifecycle
The OpenLogi architecture follows a specific lifecycle for coordination:
-
Startup: The GUI spawns the IPC client thread, which attempts connection to the agent's local socket. If absent,
SpawnReflexmay triggerlaunch::spawn_agent. -
Handshake:
ensure()validates protocol compatibility and declares the client asGui. Mismatches returnConnectFailure::NewerAgent, triggering an "Outdated GUI" banner. -
Initial Observation: The client calls
observe(0)to receive the currentAgentSnapshotimmediately, storing the returned generation number. -
Continuous Updates: The agent completes pending
observecalls whenever internal watchers (device enumeration, hook state, pairing sessions) detect changes, sending new generations to the GUI. -
Command Execution: UI actions become RPC calls multiplexed over the same connection. The tarpc transport handles concurrent request/response pairs without canceling the long-poll.
Resilience Mechanisms
The architecture includes specific safeguards for production reliability:
- Protocol Version Enforcement: If the agent reports a higher version than the GUI supports, the client returns
ConnectFailure::NewerAgent, prompting the user to restart the application. - Connection Regeneration: The generation counter lives with the connection struct (
LiveConnection.seen). When the agent restarts, the connection drops, the socket recreates, and the firstobservecall returns the full state snapshot again. - Heartbeat Timeouts:
OBSERVE_HOLD(20 seconds) plusOBSERVE_DEADLINEensure the GUI can detect agent unavailability even during periods of no state changes.
Summary
- The OpenLogi architecture strictly separates hardware access (agent) from user interface (GUI) through a tarpc-based IPC layer.
- Bincode serialization over local sockets requires immutable method ordering in the
Agenttrait to maintain wire compatibility. - The agent process exclusively owns HID++ I/O and exposes state through generation-tracked
Observationsnapshots. - The GUI client uses a single long-polling
observerequest with 20-second hold timeouts to receive real-time updates without polling overhead. - Automatic agent spawning and protocol version 29 handshake ensure the system recovers gracefully from crashes and version mismatches.
Frequently Asked Questions
What is the role of the agent process in OpenLogi?
The agent process (openlogi-agent) serves as the sole hardware gateway, owning all HID++ device I/O, input hooks, and device enumeration. It runs as a background server that responds to RPC requests from the GUI, maintaining observable state through the observe method while executing commands like set_dpi or start_pairing against physical devices.
How does the GUI receive real-time device state updates?
The GUI maintains a single in-flight observe RPC call that blocks until the agent detects state changes or the 20-second OBSERVE_HOLD timeout expires. When the agent returns an Observation with a higher generation number than previously seen, the GUI receives a GuiUpdate::Snapshot containing the complete AgentSnapshot, ensuring the UI always reflects the current system state.
What happens if the GUI and agent have different protocol versions?
During the handshake phase, the ensure() function calls protocol_version and compares the result against the GUI's compiled PROTOCOL_VERSION (currently 29). If the agent reports a higher version, the client returns ConnectFailure::NewerAgent, causing the GUI to display an "Outdated GUI" banner that requires an application restart to resolve.
Why does OpenLogi use a separate agent process instead of direct hardware access?
Separating hardware access into a distinct agent process improves security and stability by isolating privileged operations (input hooks and HID++ communication) from the GUI process. This architecture allows the GUI to run with standard user permissions while the agent handles device-level I/O, and enables automatic agent recovery and version checking without crashing the user interface.
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 →