use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex, PoisonError};
use async_hid::{AsyncHidWrite as _, Device, DeviceWriter};
use hidpp::async_trait;
use hidpp::channel::HidppChannel;
use openlogi_device::DeviceIoGate;
use openlogi_device::backend::{
BackendError, HidBackend, HotplugStream, NodeId, NodeInfo, RawWriter,
};
use crate::recording::{
RecordedChannelOpenOutcome, RecordedRawWriterOpenOutcome, RecordingRawWriter, RecordingSink,
};
use super::{
device_io_gate, enumerate_devices, is_hidpp_node, open_hidpp_channel,
open_hidpp_channel_with_observer, watch_nodes,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct HandleKey {
id: NodeId,
usage_page: u16,
usage_id: u16,
}
impl HandleKey {
fn for_device(device: &Device) -> Self {
Self {
id: super::node_id(device),
usage_page: device.usage_page,
usage_id: device.usage_id,
}
}
fn for_node(node: &NodeInfo) -> Self {
Self {
id: node.id.clone(),
usage_page: node.usage_page,
usage_id: node.usage_id,
}
}
}
static NATIVE_BACKEND: LazyLock<Arc<NativeBackend>> =
LazyLock::new(|| Arc::new(NativeBackend::default()));
pub(crate) fn native_backend() -> Arc<dyn HidBackend> {
Arc::clone(&NATIVE_BACKEND) as Arc<dyn HidBackend>
}
pub(crate) fn recording_backend(recording: RecordingSink) -> Arc<dyn HidBackend> {
Arc::new(NativeBackend::with_recording(recording))
}
pub(crate) struct NativeBackend {
nodes: Mutex<HashMap<HandleKey, Arc<Device>>>,
device_io: DeviceIoGate,
recording: Option<RecordingSink>,
}
impl Default for NativeBackend {
fn default() -> Self {
Self {
nodes: Mutex::new(HashMap::new()),
device_io: device_io_gate(),
recording: None,
}
}
}
impl NativeBackend {
fn with_recording(recording: RecordingSink) -> Self {
Self {
nodes: Mutex::new(HashMap::new()),
device_io: device_io_gate(),
recording: Some(recording),
}
}
async fn refresh(&self) -> Result<Vec<Arc<Device>>, BackendError> {
self.device_io.ensure_allowed()?;
let devices: Vec<Arc<Device>> = enumerate_devices()
.await?
.into_iter()
.map(Arc::new)
.collect();
self.device_io.ensure_allowed()?;
let handles = devices
.iter()
.map(|device| (HandleKey::for_device(device), Arc::clone(device)))
.collect();
*self.nodes.lock().unwrap_or_else(PoisonError::into_inner) = handles;
Ok(devices)
}
fn handle(&self, node: &NodeInfo) -> Result<Arc<Device>, BackendError> {
self.nodes
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&HandleKey::for_node(node))
.map(Arc::clone)
.ok_or(BackendError::Disconnected)
}
async fn open_native_raw_writer(
&self,
node: &NodeInfo,
) -> Result<NativeRawWriter, BackendError> {
self.device_io.ensure_allowed()?;
let (_reader, writer) = self
.handle(node)?
.open()
.await
.map_err(super::backend_error)?;
self.device_io.ensure_allowed()?;
Ok(NativeRawWriter {
writer,
device_io: self.device_io.clone(),
})
}
}
#[async_trait]
impl HidBackend for NativeBackend {
async fn enumerate(&self) -> Result<Vec<NodeInfo>, BackendError> {
Ok(self
.refresh()
.await?
.iter()
.map(|device| super::node_info(device))
.collect())
}
async fn enumerate_hidpp(&self) -> Result<Vec<NodeInfo>, BackendError> {
Ok(self
.refresh()
.await?
.iter()
.filter(|device| is_hidpp_node(device))
.map(|device| super::node_info(device))
.collect())
}
async fn open_hidpp(&self, node: &NodeInfo) -> Result<Option<Arc<HidppChannel>>, BackendError> {
let Some(recording) = &self.recording else {
self.device_io.ensure_allowed()?;
let device = self.handle(node)?;
return open_hidpp_channel(&device, self.device_io.clone()).await;
};
let mut capture = recording
.begin_channel(node.clone())
.map_err(|error| BackendError::Backend(error.to_string()))?;
let observer = capture.observer();
let result = match self.device_io.ensure_allowed() {
Ok(()) => match self.handle(node) {
Ok(device) => {
open_hidpp_channel_with_observer(&device, self.device_io.clone(), observer)
.await
}
Err(error) => Err(error),
},
Err(suspended) => Err(suspended.into()),
};
let outcome = match &result {
Ok(Some(channel)) => RecordedChannelOpenOutcome::Opened {
supports_short: channel.supports_short,
supports_long: channel.supports_long,
},
Ok(None) => RecordedChannelOpenOutcome::NotHidpp,
Err(error) => RecordedChannelOpenOutcome::Failed(error.to_string()),
};
capture.complete(outcome);
result
}
async fn open_raw_writer(&self, node: &NodeInfo) -> Result<Box<dyn RawWriter>, BackendError> {
let Some(recording) = &self.recording else {
return Ok(Box::new(self.open_native_raw_writer(node).await?));
};
let mut capture = recording
.begin_raw_writer(node.clone())
.map_err(|error| BackendError::Backend(error.to_string()))?;
match self.open_native_raw_writer(node).await {
Ok(writer) => {
capture.complete(RecordedRawWriterOpenOutcome::Opened);
Ok(Box::new(RecordingRawWriter::new(Box::new(writer), capture)))
}
Err(error) => {
capture.complete(RecordedRawWriterOpenOutcome::Failed(error.to_string()));
Err(error)
}
}
}
fn watch(&self) -> Result<HotplugStream, BackendError> {
Ok(Box::new(watch_nodes()?))
}
}
struct NativeRawWriter {
writer: DeviceWriter,
device_io: DeviceIoGate,
}
#[async_trait]
impl RawWriter for NativeRawWriter {
async fn write_output_report(&mut self, report: &[u8]) -> Result<(), BackendError> {
self.device_io.ensure_allowed()?;
self.writer
.write_output_report(report)
.await
.map_err(super::backend_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recording::{NativeRecorder, RecordedChannelOpenOutcome};
use openlogi_device::device_io_channel;
#[test]
fn node_handle_keys_preserve_collections_on_the_same_os_node() {
let os_node = NodeId::from("RegistryEntryId(42)".to_owned());
let primary_mouse = NodeInfo {
id: os_node.clone(),
vendor_id: 0x1234,
product_id: 0x5678,
usage_page: 0x0001,
usage_id: 0x0002,
name: "Test Mouse".to_owned(),
manufacturer: Some("Test Vendor".to_owned()),
serial_number: None,
};
let hidpp = NodeInfo {
id: os_node,
vendor_id: 0x1234,
product_id: 0x5678,
usage_page: 0xff43,
usage_id: 0x0202,
name: "Test Mouse".to_owned(),
manufacturer: Some("Test Vendor".to_owned()),
serial_number: None,
};
let hidpp_key = HandleKey::for_node(&hidpp);
let handles = HashMap::from([
(HandleKey::for_node(&primary_mouse), "primary mouse"),
(hidpp_key.clone(), "hidpp"),
]);
assert_eq!(handles.len(), 2);
assert_eq!(handles.get(&hidpp_key), Some(&"hidpp"));
}
#[tokio::test]
async fn recording_backend_retains_suspended_channel_open_failure() {
let recorder = NativeRecorder::new(8).unwrap();
let (signal, device_io) = device_io_channel();
assert!(signal.suspend());
let suspended = device_io
.ensure_allowed()
.expect_err("a suspended gate refuses device I/O")
.to_string();
let backend = NativeBackend {
nodes: Mutex::new(HashMap::new()),
device_io,
recording: Some(recorder.sink()),
};
let node = NodeInfo {
id: NodeId::from("suspended-test-node".to_owned()),
vendor_id: 0x046d,
product_id: 0xc548,
usage_page: 0xff00,
usage_id: 0x0002,
name: "Suspended Receiver".to_owned(),
manufacturer: Some("Logitech".to_owned()),
serial_number: None,
};
let Err(error) = backend.open_hidpp(&node).await else {
panic!("suspended channel open unexpectedly succeeded");
};
assert_eq!(error.to_string(), suspended);
drop(backend);
let recording = recorder.finish().unwrap();
assert_eq!(recording.channels.len(), 1);
assert!(matches!(
&recording.channels[0].open_outcome,
RecordedChannelOpenOutcome::Failed(message) if message == &suspended
));
assert!(recording.channels[0].closed_at.is_some());
}
}