How OpenLogi Handles IPC Communication: Architecture, tarpc, and Local Sockets

OpenLogi implements type-safe IPC communication using tarpc over cross-platform local sockets, serializing requests and responses with bincode to bridge the GUI client and privileged background agent.

OpenLogi is an open-source input device manager that separates its architecture into a user-facing GUI client (openlogi-desktop) and a privileged background agent (openlogi-agent). To coordinate between these processes, the project implements a robust IPC communication layer built on Rust's tarpc framework and the interprocess crate. This architecture ensures reliable, low-latency message passing across Unix systems and Windows while maintaining strict wire-protocol compatibility.

The IPC Architecture: GUI Client and Background Agent

OpenLogi splits functionality into two distinct processes that must communicate without shared memory. The IPC communication layer lives in the openlogi-ipc crate, which defines the service contract, transport mechanisms, and serialization format used for all cross-process interactions. The agent runs as a system service with elevated privileges, while the GUI connects as an unprivileged client to request state and issue configuration commands.

Service Definition with tarpc

The project uses tarpc, a remote procedure call (RPC) framework for Rust, to define a strict request/response contract between client and server at the type level.

The Agent Service Contract in ipc.rs

The core service definition resides in crates/openlogi-ipc/src/ipc.rs. Here, the #[tarpc::service] macro generates the AgentClient struct for the client side and the serve function for the server side from a Rust trait definition. Because tarpc encodes each method call as an enum variant, the declaration order of methods in this file constitutes the wire format—new methods must always be appended to maintain backward compatibility.

The Agent trait exposes operations including:

  • status() – Returns the current agent state
  • snapshot() – Retrieves a full state snapshot
  • observe(since: Generation) – Implements long-polling for state updates

Protocol Versioning and Compatibility

To prevent wire-protocol mismatches between different release versions, OpenLogi defines a PROTOCOL_VERSION constant in ipc.rs. Every new connection begins with a handshake that queries protocol_version(), ensuring both the GUI and agent speak the same version before processing substantive requests.

Transport Layer Implementation

While tarpc handles RPC semantics, the actual byte transport relies on platform-specific local sockets implemented in crates/openlogi-ipc/src/transport.rs.

Cross-Platform Local Sockets

The interprocess crate provides the underlying socket implementation that abstracts platform differences:

  • Unix: Creates a Unix-domain socket at ~/.config/openlogi/agent.sock
  • Windows: Uses a named pipe located at \\.\pipe\openlogi-agent.sock

The transport module exposes three key functions used by both sides:

  • bind() – Creates the listener on the platform-specific endpoint
  • connect() – Opens a client stream to the existing socket
  • wrap() – Encapsulates the raw stream in a tarpc-compatible transport

Length-Delimited Bincode Framing

Once connected, both sides wrap the raw stream using serde_transport::Transport configured with length-delimited Bincode framing. This guarantees deterministic, efficient binary serialization across process boundaries without schema drift or parsing ambiguity.

IPC Communication Flow

The interaction follows a strict client-server pattern with specific initialization sequences for each side.

Server-Side: Binding and Serving

The agent process initializes the IPC server by calling bind() to create the platform-specific listener. Upon accepting a connection, it wraps the stream and passes it to tarpc::server::BaseChannel::new along with the concrete Agent implementation (typically the Orchestrator struct from openlogi-agent-core).

use openlogi_ipc::transport::{bind, wrap};
use openlogi_ipc::ipc::Agent;
use openlogi_agent_core::orchestrator::Orchestrator;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = bind()?;
    let (stream, _) = listener.accept().await?;
    let transport = wrap(stream);
    let agent = Orchestrator::new();
    
    tarpc::server::BaseChannel::new(transport, agent)
        .execute()
        .await;
    Ok(())
}

Client-Side: Connecting and Calling Methods

The GUI client establishes communication through connect_and_spawn() (defined in the IPC crate's client module), which returns a ready-to-use AgentClient. This generated client exposes async methods corresponding exactly to the trait definition.

use openlogi_ipc::client::connect_and_spawn;
use openlogi_ipc::ipc::AgentClient;

async fn get_status() -> Result<(), tarpc::client::RpcError> {
    let client = connect_and_spawn().await?;
    let status = client.status().await?;
    println!("Agent status: {:?}", status);
    Ok(())
}

Long-Polling State Observation

Rather than using push-based subscriptions, OpenLogi implements a pull-based observation mechanism through the observe() method. The server holds the request for up to OBSERVE_HOLD (20 seconds), returning an Observation (containing a generation counter and state snapshot) only when the underlying state changes. This eliminates the need for complex subscription management while keeping the GUI synchronized with the agent's internal state.

async fn poll_observe(mut client: AgentClient) -> Result<(), tarpc::client::RpcError> {
    let mut gen = 0;
    loop {
        let observation = client.observe(gen).await?;
        println!("New generation {}: {:?}", observation.generation, observation.snapshot);
        gen = observation.generation;
    }
}

Summary

  • OpenLogi uses tarpc to generate type-safe RPC clients and servers from a single trait definition in crates/openlogi-ipc/src/ipc.rs.
  • The transport layer leverages local sockets (Unix domain sockets on Linux/macOS, named pipes on Windows) via the interprocess crate, implemented in crates/openlogi-ipc/src/transport.rs.
  • All messages use length-delimited bincode serialization for efficient, deterministic binary encoding across process boundaries.
  • A protocol version handshake ensures wire compatibility between different client and agent versions at connection time.
  • The observe() method implements long-polling with a 20-second hold time to synchronize state without maintaining persistent subscriptions or complex pub/sub infrastructure.

Frequently Asked Questions

What RPC framework does OpenLogi use for IPC?

OpenLogi uses tarpc, a Rust-native RPC framework that generates client stubs and server implementations from a trait annotated with #[tarpc::service] in crates/openlogi-ipc/src/ipc.rs. This provides compile-time type safety for all cross-process calls while handling serialization and transport concerns transparently.

How does OpenLogi ensure backward compatibility in IPC?

OpenLogi maintains backward compatibility by treating the method order in the tarpc service trait as immutable. Because tarpc encodes methods as enum variants, new RPC methods must always be appended to the trait definition in crates/openlogi-ipc/src/ipc.rs. Additionally, a protocol_version() handshake validates that both processes speak the same wire protocol before processing substantive requests.

What transport mechanism does OpenLogi use for cross-platform IPC?

The project uses the interprocess crate to abstract platform differences. On Unix systems, it creates a Unix-domain socket at ~/.config/openlogi/agent.sock; on Windows, it uses a named pipe at \\.\pipe\openlogi-agent.sock. The transport code unifies these behind a common interface in crates/openlogi-ipc/src/transport.rs.

How does the GUI receive real-time updates from the agent?

Instead of push-based pub/sub, OpenLogi implements a long-polling mechanism via the observe(since: Generation) method. The GUI requests observations starting from a specific generation number, and the server holds the request for up to 20 seconds (OBSERVE_HOLD), returning immediately when the state changes. This approach simplifies the architecture while providing near real-time synchronization without persistent connections.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →