AprilNEA/OpenLogi · warning · io::Error

host device I/O is suspended

Error message

host device I/O is suspended

What it means

`openlogi-hid`'s transport exposes a `DeviceIoGate` so the host can suspend HID writes (e.g. while the agent is not the foreground owner or during permission/teardown states). When a `write_report` arrives through `device_io_error`/`device_io_suspended` while the gate is closed, the backend reports `BackendError::Backend("host device I/O is suspended")`. It is a deliberate backpressure signal, not a device malfunction.

Solutions

  1. Wait for the suspend to lift and retry the write — this is `WouldBlock` semantics, a retry is the correct response.
  2. Subscribe to `DeviceIoSignal` and queue writes until the gate reopens instead of issuing them while suspended.
  3. Check whether another component deliberately suspended I/O (pairing, exclusive capture, permission flow) and coordinate with it.
  4. If suspensions are unexpected, verify agent state transitions — a stuck gate indicates an owner never resumed I/O.

Example fix

// before — fire-and-forget write that fails while suspended
channel.write_report(&report).await?;

// after — wait for I/O to resume, then write
device_io_channel().resumed().await; // or poll the gate state
channel.write_report(&report).await?;
Defensive patterns

Strategy: retry

Validate before calling

// check the gate before issuing writes
if device_io_gate().is_suspended() {
    // queue the write; do not call write_report yet
}

Try / catch

match channel.write_report(&report).await {
    Err(err) if err.to_string().contains("host device I/O is suspended") => {
        // WouldBlock semantics: wait for the DeviceIoSignal resume, then retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling `write_report` (via the HID++ channel) while the `DeviceIoGate` is suspended — the gate's error constructor builds an `io::ErrorKind::WouldBlock` error carrying this message, surfaced as a `BackendError::Backend`.

Common situations: Agent performing DPI/config writes during a suspension window (e.g. permission revocation, host pause, another process owning the device, or an internal pause like pairing/capture); a queued command from the GUI racing a suspend/resume transition.

Related errors


AI-assisted analysis of AprilNEA/OpenLogi@e846e6f4b4 (2026-09-13). Data as JSON: /api/errors/e88c61f182aee99f. Report an issue: GitHub.

Appendix: source

Thrown at crates/openlogi-hid/src/transport.rs:56

/// now that the contract lives in `openlogi-device`, which is the orphan rule
/// saying out loud what the layering already did — an adapter belongs to the
/// backend it adapts. `Disconnected` and `NotConnected` fold together; nothing
/// above the transport acts on the distinction.
fn backend_error(error: async_hid::HidError) -> BackendError {
    match error {
        async_hid::HidError::Disconnected | async_hid::HidError::NotConnected => {
            BackendError::Disconnected
        }
        other => BackendError::Backend(other.to_string()),
    }
}

fn device_io_suspended() -> BackendError {
    BackendError::Backend("host device I/O is suspended".into())
}

fn device_io_error() -> Box<dyn Error + Send + Sync> {
    std::io::Error::new(
        std::io::ErrorKind::WouldBlock,
        "host device I/O is suspended",
    )
    .into()
}

/// Classify a failed device open. On macOS `IOHIDDeviceOpen` denies silently —
/// the error is indistinguishable from exclusive access — so fold the Input
/// Monitoring state into the message: it is the difference between "grant the
/// permission" and "close the other app, or log out and back in".
#[cfg(not(target_os = "windows"))]
fn open_error(error: async_hid::HidError) -> BackendError {
    match backend_error(error) {
        #[cfg(target_os = "macos")]
        BackendError::Backend(message) => {
            let hint = if crate::permissions::has_access() {
                "Input Monitoring is granted to this process — another app may \
                 hold the device exclusively, or macOS is serving a stale \

View on GitHub (pinned to e846e6f4b4)