# How openlogi-desktop Communicates with openlogi-agent: IPC Architecture Explained

> Understand how openlogi-desktop communicates with openlogi-agent via IPC sockets and the openlogi-ipc crate for seamless device command and state synchronization. Explore the architecture.

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

---

**OpenLogi-desktop never interacts directly with hardware; instead, it communicates with a background openlogi-agent process over a local IPC socket, using the openlogi-ipc crate for all device command and state synchronization.**

The OpenLogi project implements a clear **separation of concerns**: the GUI (openlogi-desktop) handles user interaction and rendering, while the agent (openlogi-agent) owns all HID++ I/O, input-hook handling, and device-level logic. This article explains how these two processes communicate through a **local IPC socket**, based on the implementation in the AprilNEA/OpenLogi repository.

## IPC Connection Model: Client Thread Architecture

The GUI spawns a dedicated **IPC client thread** via `IpcClient::spawn` in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs). This thread runs its own Tokio runtime and manages the openlogi-desktop to openlogi-agent connection.

The client thread performs three critical operations:

- **Connects to the agent** using `openlogi_ipc::client::connect`
- **Declares its client type** as `ClientKind::Gui` during the handshake
- **Maintains a long-poll** (`observe`) to receive full agent state snapshots whenever they change

The client thread also receives commands from the GUI via an `mpsc::UnboundedSender<Command>` and forwards them to openlogi-agent over the same connection. This design keeps the GUI responsive while the IPC thread handles network I/O.

## Agent Launch Reflex: When and How Desktop Starts the Agent

When the IPC socket is unreachable, openlogi-desktop must decide whether to launch openlogi-agent itself. This logic resides in [`crates/openlogi-desktop/src/services/ipc/launch.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc/launch.rs).

The `SpawnReflex` mechanism handles three distinct situations:

| Situation | Action |
|-----------|--------|
| **Cold start** – no connection ever existed | `spawn_agent` is called immediately |
| **Lost connection** – crash or supervised restart | Waits `SPAWN_AFTER_LOSS` (≈ 2s) before spawning, allowing system services to restart the agent first |
| **Agent newer than GUI** | No spawn; displays *OutdatedGui* banner and expects user to relaunch |

### Platform-Specific Launch Behavior

On **macOS**, the launch sequence tries `launchctl kickstart` on a registered launch item first. If that fails, it attempts to register the login item and kick-start again. If registration is impossible (user disabled login items), the GUI logs a warning and **does not** start an unmanaged agent.

On **non-macOS platforms**, the GUI simply executes the sibling binary next to itself (`openlogi-agent{EXE_SUFFIX}`) or the packaged login-item bundle.

## Message Flow Between Desktop and Agent

### GUI → Agent: Commands

Device commands flow from openlogi-desktop to openlogi-agent as variants of the `Command` enum defined in [`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs):

```rust
// Send a DPI change command to the agent
ipc.commands.send(Command::SetDpi(device_route, new_dpi));

// Control device lighting
ipc.commands.send(Command::SetLight(device_route, light_config));

// Initiate device pairing
ipc.commands.send(Command::StartPairing);

```

Configuration reload requests and accessibility prompts are also encoded as `Command` variants.

### Agent → GUI: State Snapshots

The agent delivers complete state information via `GuiUpdate::Snapshot`:

```rust
while let Some(update) = ipc.updates.recv().await {
    match update {
        GuiUpdate::Snapshot(state) => {
            // Contains full device inventory, DPI settings, light states
            // GUI replaces entire state—no incremental reconciliation needed
        }
        // ...
    }
}

```

Because snapshots are **whole-state replacements**, the GUI implementation remains simple; it never needs to merge partial updates or track deltas.

### Error Handling

When openlogi-agent becomes unreachable, the IPC client emits:

- `GuiUpdate::Unreachable` – connection lost
- `GuiUpdate::OutdatedGui` – version mismatch detected
- `WriteError::AgentUnavailable` – synthetic error for pending commands

## Complete Lifecycle Overview

1. **Startup** – Desktop binary creates the IPC client thread via `IpcClient::spawn`
2. **Connection attempt** – Client tries to connect; invokes `launch::spawn_agent` if needed
3. **Normal operation** – Long-poll remains open; state snapshots flow to GUI, commands flow to agent
4. **Agent disruption** – Client detects dropped connection, updates `SpawnReflex` via `reflex.lost()`, retries launch after delay
5. **Shutdown** – GUI drops command sender; client thread exits cleanly

## Key Source Files and Their Roles

| File | Purpose |
|------|---------|
| [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) | Core IPC client, command handling, `observe_loop` |
| [`crates/openlogi-desktop/src/services/ipc/launch.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc/launch.rs) | Agent launch/kick-start logic with platform support |
| [`crates/openlogi-agent/src/main.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/main.rs) | Agent entry point; creates IPC server |
| [`crates/openlogi-ipc/src/client.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/client.rs) | Low-level tarpc client implementation |
| [`crates/openlogi-ipc/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/transport.rs) | Socket location: `openlogi-agent.sock` |

## Practical Code Example

```rust
use openlogi_desktop::services::ipc::{spawn, Command, GuiUpdate};

// 1. Initialize the IPC client thread
let ipc = spawn();

// 2. Send device commands asynchronously
ipc.commands
    .send(Command::SetDpi(device_id, 1600))
    .unwrap();

// 3. Process state updates on the main thread
tokio::spawn(async move {
    while let Some(update) = ipc.updates.recv().await {
        match update {
            GuiUpdate::Snapshot(full_state) => {
                refresh_device_list(full_state.devices);
            }
            GuiUpdate::Unreachable => show_agent_warning(),
            GuiUpdate::OutdatedGui => prompt_restart_dialog(),
            _ => {}
        }
    }
});

```

This pattern—spawn once, send commands via channel, react to updates—runs throughout the openlogi-desktop codebase.

## Summary

- **openlogi-desktop and openlogi-agent are separate processes** communicating over a local socket, not direct function calls
- The **IPC client thread** (`IpcClient::spawn`) isolates async I/O from the GUI main thread
- **Whole-state snapshots** eliminate complex state synchronization logic in the GUI
- **Platform-aware launch reflex** handles cold starts, crashes, and version mismatches appropriately
- All implementation lives in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs) and its [`launch.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/launch.rs) submodule

## Frequently Asked Questions

### What happens if openlogi-agent crashes while the GUI is running?

The IPC client thread detects the dropped connection, calls `reflex.lost()` on its `SpawnReflex`, and waits approximately 2 seconds (`SPAWN_AFTER_LOSS`) before attempting to restart the agent. This delay allows any system service supervisor to recover the agent first. The GUI receives `GuiUpdate::Unreachable` during this window and can display appropriate status to the user.

### Why doesn't openlogi-desktop talk directly to Logitech devices?

Direct hardware access requires elevated privileges, kernel-level HID++ protocol handling, and global input hook management—operations that are risky to perform in a GUI process. By isolating all device I/O in openlogi-agent, the project achieves better security sandboxing, cleaner crash boundaries, and the ability to run the agent as a system service independently of the GUI.

### What protocol carries the actual IPC messages?

The openlogi-ipc crate implements the transport using **tarpc** over a platform-specific Unix domain socket (or named pipe on Windows) located at `openlogi-agent.sock`. The `openlogi_ipc::client::connect` function in [`crates/openlogi-ipc/src/client.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/client.rs) establishes this connection, which the desktop then uses for bidirectional communication with the agent's IPC server.

### How does the GUI know if it's too old for the running agent?

During the initial handshake, the agent reports its protocol version. If the agent's version exceeds what the GUI supports, the IPC client emits `GuiUpdate::OutdatedGui` and **refuses** to spawn a new agent. The GUI displays a banner prompting the user to update and restart the application, preventing protocol mismatch errors that would cause silent failures.