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

> Discover how OpenLogi handles IPC communication with type-safe tarpc over local sockets. Learn about its architecture, bincode serialization, and bridging the GUI client and background agent.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: architecture
- Published: 2026-08-22

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs)

The core service definition resides in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`).

```rust
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.

```rust
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.

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.