{"record":{"id":"e88c61f182aee99f","repo":"AprilNEA/OpenLogi","slug":"host-device-i-o-is-suspended","errorCode":null,"errorMessage":"host device I/O is suspended","messagePattern":"host device I/O is suspended","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"warning","filePath":"crates/openlogi-hid/src/transport.rs","lineNumber":56,"sourceCode":"/// now that the contract lives in `openlogi-device`, which is the orphan rule\n/// saying out loud what the layering already did — an adapter belongs to the\n/// backend it adapts. `Disconnected` and `NotConnected` fold together; nothing\n/// above the transport acts on the distinction.\nfn backend_error(error: async_hid::HidError) -> BackendError {\n    match error {\n        async_hid::HidError::Disconnected | async_hid::HidError::NotConnected => {\n            BackendError::Disconnected\n        }\n        other => BackendError::Backend(other.to_string()),\n    }\n}\n\nfn device_io_suspended() -> BackendError {\n    BackendError::Backend(\"host device I/O is suspended\".into())\n}\n\nfn device_io_error() -> Box<dyn Error + Send + Sync> {\n    std::io::Error::new(\n        std::io::ErrorKind::WouldBlock,\n        \"host device I/O is suspended\",\n    )\n    .into()\n}\n\n/// Classify a failed device open. On macOS `IOHIDDeviceOpen` denies silently —\n/// the error is indistinguishable from exclusive access — so fold the Input\n/// Monitoring state into the message: it is the difference between \"grant the\n/// permission\" and \"close the other app, or log out and back in\".\n#[cfg(not(target_os = \"windows\"))]\nfn open_error(error: async_hid::HidError) -> BackendError {\n    match backend_error(error) {\n        #[cfg(target_os = \"macos\")]\n        BackendError::Backend(message) => {\n            let hint = if crate::permissions::has_access() {\n                \"Input Monitoring is granted to this process — another app may \\\n                 hold the device exclusively, or macOS is serving a stale \\","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/AprilNEA/OpenLogi/blob/e846e6f4b4405e33bd6a9aaf949a482ce34cb6d8/crates/openlogi-hid/src/transport.rs#L38-L74","documentation":"`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.","triggerScenarios":"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`.","commonSituations":"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.","solutions":["Wait for the suspend to lift and retry the write — this is `WouldBlock` semantics, a retry is the correct response.","Subscribe to `DeviceIoSignal` and queue writes until the gate reopens instead of issuing them while suspended.","Check whether another component deliberately suspended I/O (pairing, exclusive capture, permission flow) and coordinate with it.","If suspensions are unexpected, verify agent state transitions — a stuck gate indicates an owner never resumed I/O."],"exampleFix":"// before — fire-and-forget write that fails while suspended\nchannel.write_report(&report).await?;\n\n// after — wait for I/O to resume, then write\ndevice_io_channel().resumed().await; // or poll the gate state\nchannel.write_report(&report).await?;","handlingStrategy":"retry","validationCode":"// check the gate before issuing writes\nif device_io_gate().is_suspended() {\n    // queue the write; do not call write_report yet\n}","typeGuard":null,"tryCatchPattern":"match channel.write_report(&report).await {\n    Err(err) if err.to_string().contains(\"host device I/O is suspended\") => {\n        // WouldBlock semantics: wait for the DeviceIoSignal resume, then retry\n    }\n    other => other?,\n}","preventionTips":["Subscribe to DeviceIoSignal and only write while the gate is open.","Queue HID++ writes during suspension windows instead of failing.","Verify no component leaves the gate suspended after pairing/permission flows."],"tags":["hid","backpressure","suspension","retry"],"backgroundTag":"resource-temporarily-unavailable","analyzedSha":"e846e6f4b4405e33bd6a9aaf949a482ce34cb6d8","analyzedAt":"2026-09-13T03:07:16.451Z","contentChangedAt":"2026-09-13T03:07:16.451Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}