How OpenLogi's IPC Layer Connects the Agent, GUI, and Overlay Processes
OpenLogi uses a tarpc-based binary-coded RPC protocol over local Unix sockets (or Windows named pipes) to enable secure, type-safe communication between the hardware-owning Agent, the Desktop GUI, and the Overlay helper process.
OpenLogi's architecture isolates hardware I/O within a dedicated Agent process while delegating user interaction to separate GUI and Overlay components. The OpenLogi IPC layer bridges these processes using a Rust tarpc service definition that exposes device state and control commands through a local socket transport. This design ensures that only the privileged Agent accesses HID devices, while the unprivileged GUI and Overlay clients observe state and issue commands through a version-checked, multiplexed RPC channel.
Transport Layer: Local Sockets and tarpc
The foundation of the OpenLogi IPC layer relies on platform-specific local socket transports wrapped with tarpc's typed RPC framework. In crates/openlogi-ipc/src/transport.rs, the connect() function establishes either a Unix domain socket at /tmp/openlogi.sock (macOS/Linux) or a Windows named pipe, then wraps the stream using transport::wrap to create a framed tarpc transport.
Clients receive a generated AgentClient from this transport, which implements the Agent trait defined in crates/openlogi-ipc/src/ipc.rs. This trait serves as the exclusive contract for all cross-process communication, ensuring type safety through bincode serialization.
Connection Handshake and Client Declaration
Before issuing commands, every client must complete a mandatory three-step handshake enforced by the protocol:
-
Protocol Version Check — The first RPC call is always
protocol_version()(method 0). Because tarpc encodes method order, this check guarantees both sides share the same bincode schema. A mismatch results in the GUI displayingOutdatedGuior the overlay exiting immediately. -
Client Kind Declaration — After a successful handshake, the client calls
declare_client(kind), passing eitherClientKind::GuiorClientKind::Overlay. The agent only arms a dormant component after this call, preventing stray processes from unintentionally waking a sleeping agent. -
Identity Token (Overlay Only) — The overlay additionally calls
identity()to obtain a run-token and watches for supersession via thesuccessioncrate, ensuring only one overlay instance controls the action ring.
Agent Process: The Hardware Authority
The Agent is the sole owner of all HID and device I/O within the OpenLogi architecture. It runs a tarpc server that implements the Agent trait from crates/openlogi-ipc/src/ipc.rs, started by the openlogi-agent binary. The Agent maintains the canonical state of connected devices and exposes this state exclusively through RPC methods, never allowing direct hardware access from client processes.
GUI Client: Observing State and Sending Commands
The Desktop GUI, built on the GPUI framework, manages its IPC connection in crates/openlogi-desktop/src/services/ipc.rs. The spawn() function creates a dedicated background thread running a Tokio runtime that maintains a long-lived connection to the Agent.
// crates/openlogi-desktop/src/services/ipc.rs
pub fn spawn() -> IpcClient {
let (update_tx, updates) = mpsc::unbounded_channel();
let (commands, mut cmd_rx) = mpsc::unbounded_channel::<Command>();
std::thread::Builder::new()
.name("openlogi-ipc-client".into())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime init failed");
rt.block_on(async { observe_loop(&update_tx, &mut cmd_rx).await });
})
.expect("failed to spawn IPC client thread");
IpcClient { updates, commands }
}
The GUI client implements a generation-driven observation loop. The observe_loop function maintains a continuous Agent::observe request that carries the last seen generation ID. The agent replies immediately when the AgentSnapshot changes, or after OBSERVE_HOLD (20 seconds) as a heartbeat. This eliminates polling overhead while ensuring the GUI receives state updates within milliseconds of hardware changes.
Device control commands—such as set_dpi, set_light, or start_pairing—travel from the GUI to the Agent via the same multiplexed connection, interleaved with the ongoing observation request.
Overlay Client: Action Ring Coordination
The Overlay process, responsible for rendering the action ring interface, uses a specialized subset of the OpenLogi IPC layer defined in crates/openlogi-overlay/src/agent.rs. Unlike the GUI, the overlay does not receive full device snapshots; instead, it polls observe_action_ring to receive ActionRingInvocation state.
// crates/openlogi-overlay/src/agent.rs
async fn poll_invocations(tx: mpsc::UnboundedSender<Option<ActionRingInvocation>>) {
let mut state = InvocationPollState::default();
loop {
if matches!(&state, InvocationPollState::Reconnecting { .. }) {
if let Some(client) = connect().await {
state.connected(client);
} else {
if state.connection_failed(Instant::now()) {
stand_down("no agent answered for 1 min");
}
tokio::time::sleep(RETRY_PERIOD).await;
continue;
}
}
let Some((client, seen)) = state.observation() else { continue };
let mut ctx = context::current();
ctx.deadline = Instant::now() + OBSERVE_HOLD + Duration::from_secs(5);
match client.observe_action_ring(ctx, seen).await {
Ok(observed) if observed.generation != seen => {
state.observed(observed.generation);
let _ = tx.send(observed.invocation);
}
Ok(_) => continue,
Err(_) => state.disconnected(),
}
}
}
When the user interacts with the ring, the overlay sends discrete commands back to the Agent:
async fn send_command(client: &AgentClient, command: OverlayCommand) -> bool {
let ctx = context::current();
match command {
OverlayCommand::Hover { session_id, slot } => {
client.action_ring_hover(ctx, session_id, slot).await.is_ok()
}
OverlayCommand::Activate { session_id, slot } => {
client.action_ring_activate(ctx, session_id, slot).await.is_ok()
}
OverlayCommand::Cancel { session_id } => {
client.action_ring_cancel(ctx, session_id).await.is_ok()
}
}
}
Error Recovery and Transport Resilience
Both the GUI and Overlay treat transport failures as disconnect events. When the tarpc stream fails, the client drops the current LiveConnection, fires a reconnection attempt using openlogi_ipc::client::connect, and propagates status to the user interface. The GUI displays GuiUpdate::Unreachable, while the overlay silently retries commands until the Agent acknowledges them or a timeout expires (typically one minute of total silence).
This design ensures that temporary agent restarts or socket interruptions do not crash the GUI or Overlay; instead, they enter a graceful reconnection loop that recovers automatically when the Agent socket becomes available again.
Summary
- OpenLogi's IPC layer implements a tarpc-based RPC protocol over local Unix sockets or Windows named pipes, defined in
crates/openlogi-ipc/src/ipc.rs. - The Agent process exclusively owns HID hardware and exposes state through the
Agenttrait, while the GUI and Overlay act as unprivileged clients. - All connections begin with a mandatory
protocol_version()handshake followed bydeclare_client()to identify as eitherClientKind::GuiorClientKind::Overlay. - The GUI synchronizes state via a generation-driven
observe()long-poll that returns immediately on changes or after a 20-second hold timeout. - The Overlay uses
observe_action_ring()to receive ring-specific state and sends user interactions viaaction_ring_hover,action_ring_activate, andaction_ring_cancel. - Transport failures trigger automatic reconnection logic, ensuring resilient communication without process restarts.
Frequently Asked Questions
What transport protocol does OpenLogi use for inter-process communication?
OpenLogi uses tarpc over local sockets for IPC. On macOS and Linux, it creates a Unix domain socket at /tmp/openlogi.sock; on Windows, it uses a named pipe. The transport is wrapped with tarpc's framing to provide type-safe RPC over bincode serialization.
How does the GUI application stay synchronized with the Agent's device state?
The GUI maintains a long-polling observation loop via the Agent::observe RPC method. It sends the last known generation ID; the Agent responds immediately when the AgentSnapshot changes, or after 20 seconds (OBSERVE_HOLD) as a heartbeat. This push-style mechanism eliminates polling overhead while ensuring sub-second synchronization.
What happens if the GUI and Agent have incompatible protocol versions?
During the initial handshake, the client calls protocol_version() as the first RPC. If the returned version does not match the client's expected schema, the GUI displays an OutdatedGui error message, and the overlay exits cleanly. This prevents serialization errors and undefined behavior from mismatched bincode definitions.
Can multiple Overlay instances connect to the same Agent simultaneously?
While the protocol supports multiple connections, the Overlay calls identity() to obtain a unique run-token and monitors for supersession via the succession crate. This mechanism ensures that only one Overlay instance controls the action ring at any given time, with older instances gracefully standing down when a new connection claims authority.
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 →