mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
828 lines
28 KiB
Rust
828 lines
28 KiB
Rust
//! HID path: browser (WebSocket or WebRTC DataChannel) → queue → OTG gadget or CH9329.
|
|
|
|
pub mod backend;
|
|
#[cfg(target_os = "linux")]
|
|
mod bluetooth;
|
|
pub mod ch9329;
|
|
mod ch9329_proto;
|
|
pub mod consumer;
|
|
pub mod datachannel;
|
|
mod factory;
|
|
pub mod keyboard;
|
|
mod macos_drag;
|
|
#[cfg(unix)]
|
|
pub mod otg;
|
|
#[cfg(unix)]
|
|
mod otg_device;
|
|
pub mod types;
|
|
pub mod websocket;
|
|
|
|
pub use crate::events::LedState;
|
|
pub use backend::{HidBackend, HidBackendRuntimeSnapshot, HidBackendType};
|
|
pub use keyboard::CanonicalKey;
|
|
pub use types::{
|
|
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
|
|
MouseEventType,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct HidRuntimeState {
|
|
pub available: bool,
|
|
pub backend: String,
|
|
pub initialized: bool,
|
|
pub online: bool,
|
|
pub supports_absolute_mouse: bool,
|
|
pub keyboard_leds_enabled: bool,
|
|
pub led_state: LedState,
|
|
pub screen_resolution: Option<(u32, u32)>,
|
|
pub device: Option<String>,
|
|
pub error: Option<String>,
|
|
pub error_code: Option<String>,
|
|
}
|
|
|
|
impl HidRuntimeState {
|
|
fn from_backend_type(backend_type: &HidBackendType) -> Self {
|
|
Self {
|
|
available: !matches!(backend_type, HidBackendType::None),
|
|
backend: backend_type.name_str().to_string(),
|
|
initialized: false,
|
|
online: false,
|
|
supports_absolute_mouse: false,
|
|
keyboard_leds_enabled: false,
|
|
led_state: LedState::default(),
|
|
screen_resolution: None,
|
|
device: device_for_backend_type(backend_type),
|
|
error: None,
|
|
error_code: None,
|
|
}
|
|
}
|
|
|
|
fn from_backend(backend_type: &HidBackendType, snapshot: HidBackendRuntimeSnapshot) -> Self {
|
|
Self {
|
|
available: !matches!(backend_type, HidBackendType::None),
|
|
backend: backend_type.name_str().to_string(),
|
|
initialized: snapshot.initialized,
|
|
online: snapshot.online,
|
|
supports_absolute_mouse: snapshot.supports_absolute_mouse,
|
|
keyboard_leds_enabled: snapshot.keyboard_leds_enabled,
|
|
led_state: snapshot.led_state,
|
|
screen_resolution: snapshot.screen_resolution,
|
|
device: snapshot
|
|
.device
|
|
.or_else(|| device_for_backend_type(backend_type)),
|
|
error: snapshot.error,
|
|
error_code: snapshot.error_code,
|
|
}
|
|
}
|
|
|
|
fn with_error(
|
|
backend_type: &HidBackendType,
|
|
current: &Self,
|
|
reason: impl Into<String>,
|
|
error_code: impl Into<String>,
|
|
) -> Self {
|
|
let mut next = current.clone();
|
|
next.available = !matches!(backend_type, HidBackendType::None);
|
|
next.backend = backend_type.name_str().to_string();
|
|
next.initialized = false;
|
|
next.online = false;
|
|
next.keyboard_leds_enabled = false;
|
|
next.led_state = LedState::default();
|
|
next.device = device_for_backend_type(backend_type);
|
|
next.error = Some(reason.into());
|
|
next.error_code = Some(error_code.into());
|
|
next
|
|
}
|
|
}
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::RwLock;
|
|
use tracing::{info, warn};
|
|
|
|
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
|
|
use crate::error::{AppError, Result};
|
|
use crate::events::EventBus;
|
|
#[cfg(unix)]
|
|
use crate::otg::OtgService;
|
|
use factory::HidBackendFactory;
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::Mutex;
|
|
use tokio::task::JoinHandle;
|
|
|
|
const HID_EVENT_QUEUE_CAPACITY: usize = 64;
|
|
const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30;
|
|
|
|
#[derive(Debug)]
|
|
enum QueuedHidEvent {
|
|
Keyboard(KeyboardEvent),
|
|
Mouse(MouseEvent),
|
|
Consumer(ConsumerEvent),
|
|
Reset,
|
|
}
|
|
|
|
pub struct HidController {
|
|
backend_factory: HidBackendFactory,
|
|
backend: Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
|
backend_type: Arc<RwLock<HidBackendType>>,
|
|
events: Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
|
runtime_state: Arc<RwLock<HidRuntimeState>>,
|
|
hid_tx: mpsc::Sender<QueuedHidEvent>,
|
|
hid_rx: Mutex<Option<mpsc::Receiver<QueuedHidEvent>>>,
|
|
pending_move: Arc<parking_lot::Mutex<Option<MouseEvent>>>,
|
|
pending_move_flag: Arc<AtomicBool>,
|
|
hid_worker: Mutex<Option<JoinHandle<()>>>,
|
|
runtime_worker: Mutex<Option<JoinHandle<()>>>,
|
|
backend_available: Arc<AtomicBool>,
|
|
reset_requested: Arc<AtomicBool>,
|
|
screen_resolution: parking_lot::RwLock<(u32, u32)>,
|
|
}
|
|
|
|
impl HidController {
|
|
#[cfg(unix)]
|
|
pub fn new(backend_type: HidBackendType, otg_service: Option<Arc<OtgService>>) -> Self {
|
|
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
|
|
Self {
|
|
backend: Arc::new(RwLock::new(None)),
|
|
backend_factory: HidBackendFactory::new(otg_service),
|
|
backend_type: Arc::new(RwLock::new(backend_type.clone())),
|
|
events: Arc::new(tokio::sync::RwLock::new(None)),
|
|
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
|
|
&backend_type,
|
|
))),
|
|
hid_tx,
|
|
hid_rx: Mutex::new(Some(hid_rx)),
|
|
pending_move: Arc::new(parking_lot::Mutex::new(None)),
|
|
pending_move_flag: Arc::new(AtomicBool::new(false)),
|
|
hid_worker: Mutex::new(None),
|
|
runtime_worker: Mutex::new(None),
|
|
backend_available: Arc::new(AtomicBool::new(false)),
|
|
reset_requested: Arc::new(AtomicBool::new(false)),
|
|
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
pub fn new(backend_type: HidBackendType) -> Self {
|
|
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
|
|
Self {
|
|
backend: Arc::new(RwLock::new(None)),
|
|
backend_factory: HidBackendFactory::new(),
|
|
backend_type: Arc::new(RwLock::new(backend_type.clone())),
|
|
events: Arc::new(tokio::sync::RwLock::new(None)),
|
|
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
|
|
&backend_type,
|
|
))),
|
|
hid_tx,
|
|
hid_rx: Mutex::new(Some(hid_rx)),
|
|
pending_move: Arc::new(parking_lot::Mutex::new(None)),
|
|
pending_move_flag: Arc::new(AtomicBool::new(false)),
|
|
hid_worker: Mutex::new(None),
|
|
runtime_worker: Mutex::new(None),
|
|
backend_available: Arc::new(AtomicBool::new(false)),
|
|
reset_requested: Arc::new(AtomicBool::new(false)),
|
|
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub fn set_bond_store(&self, store: crate::db::hid_bonds::HidBondStore) {
|
|
let _ = self.backend_factory.bonds.set(Arc::new(store));
|
|
}
|
|
|
|
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
|
|
*self.events.write().await = Some(events);
|
|
}
|
|
|
|
pub async fn init(&self) -> Result<()> {
|
|
let backend_type = self.backend_type.read().await.clone();
|
|
let backend = match self.backend_factory.create_initialized(&backend_type).await {
|
|
Ok(Some(backend)) => backend,
|
|
Ok(None) => return Ok(()),
|
|
Err(error) => {
|
|
self.backend_available.store(false, Ordering::Release);
|
|
let current = self.runtime_state.read().await.clone();
|
|
let error_state = HidRuntimeState::with_error(
|
|
&backend_type,
|
|
¤t,
|
|
format!("Failed to initialize HID backend: {}", error),
|
|
"init_failed",
|
|
);
|
|
self.apply_runtime_state(error_state).await;
|
|
return Err(error);
|
|
}
|
|
};
|
|
|
|
{
|
|
let mut slot = self.backend.write().await;
|
|
let (width, height) = *self.screen_resolution.read();
|
|
backend.set_screen_resolution(width, height);
|
|
*slot = Some(backend);
|
|
}
|
|
self.sync_runtime_state_from_backend().await;
|
|
|
|
self.start_event_worker().await;
|
|
self.restart_runtime_worker().await;
|
|
|
|
info!("HID backend initialized: {:?}", backend_type);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn shutdown(&self) -> Result<()> {
|
|
info!("Shutting down HID controller");
|
|
self.stop_runtime_worker().await;
|
|
|
|
if let Some(backend) = self.backend.write().await.take() {
|
|
if let Err(e) = backend.shutdown().await {
|
|
warn!("Error shutting down HID backend: {}", e);
|
|
}
|
|
}
|
|
self.backend_available.store(false, Ordering::Release);
|
|
let backend_type = self.backend_type.read().await.clone();
|
|
let mut shutdown_state = HidRuntimeState::from_backend_type(&backend_type);
|
|
if matches!(backend_type, HidBackendType::None) {
|
|
shutdown_state.available = false;
|
|
} else {
|
|
shutdown_state.error = Some("HID backend stopped".to_string());
|
|
shutdown_state.error_code = Some("shutdown".to_string());
|
|
}
|
|
self.apply_runtime_state(shutdown_state).await;
|
|
|
|
info!("HID controller shutdown complete");
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn prepare_otg_rebuild(&self) -> Result<()> {
|
|
let backend_type = self.backend_type.read().await.clone();
|
|
if !matches!(backend_type, HidBackendType::Otg { .. }) {
|
|
return Ok(());
|
|
}
|
|
|
|
info!("Preparing OTG HID backend for gadget rebuild");
|
|
self.backend_available.store(false, Ordering::Release);
|
|
self.stop_runtime_worker().await;
|
|
|
|
if let Some(backend) = self.backend.write().await.take() {
|
|
backend.prepare_rebuild().await?;
|
|
}
|
|
|
|
let current = self.runtime_state.read().await.clone();
|
|
let rebuilding_state = HidRuntimeState::with_error(
|
|
&backend_type,
|
|
¤t,
|
|
"OTG gadget is rebuilding",
|
|
"rebuilding",
|
|
);
|
|
self.apply_runtime_state(rebuilding_state).await;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
|
|
if !self.backend_available.load(Ordering::Acquire) {
|
|
return Err(AppError::BadRequest(
|
|
"HID backend not available".to_string(),
|
|
));
|
|
}
|
|
self.enqueue_event(QueuedHidEvent::Keyboard(event)).await
|
|
}
|
|
|
|
pub async fn send_mouse(&self, event: MouseEvent) -> Result<()> {
|
|
if !self.backend_available.load(Ordering::Acquire) {
|
|
return Err(AppError::BadRequest(
|
|
"HID backend not available".to_string(),
|
|
));
|
|
}
|
|
|
|
if matches!(
|
|
event.event_type,
|
|
MouseEventType::Move | MouseEventType::MoveAbs
|
|
) {
|
|
self.enqueue_mouse_move(event)
|
|
} else {
|
|
self.enqueue_event(QueuedHidEvent::Mouse(event)).await
|
|
}
|
|
}
|
|
|
|
pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
|
|
if !self.backend_available.load(Ordering::Acquire) {
|
|
return Err(AppError::BadRequest(
|
|
"HID backend not available".to_string(),
|
|
));
|
|
}
|
|
self.enqueue_event(QueuedHidEvent::Consumer(event)).await
|
|
}
|
|
|
|
pub async fn bluetooth_status(&self) -> Result<serde_json::Value> {
|
|
let backend = self
|
|
.backend
|
|
.read()
|
|
.await
|
|
.clone()
|
|
.ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?;
|
|
backend.bluetooth_status().await
|
|
}
|
|
pub async fn bluetooth_action(&self, action: &str, seconds: u32) -> Result<()> {
|
|
let backend = self
|
|
.backend
|
|
.read()
|
|
.await
|
|
.clone()
|
|
.ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?;
|
|
backend.bluetooth_action(action, seconds).await
|
|
}
|
|
|
|
pub async fn reset(&self) -> Result<()> {
|
|
if !self.backend_available.load(Ordering::Acquire) {
|
|
return Ok(());
|
|
}
|
|
self.enqueue_event(QueuedHidEvent::Reset).await
|
|
}
|
|
|
|
pub async fn is_available(&self) -> bool {
|
|
self.backend_available.load(Ordering::Acquire)
|
|
}
|
|
|
|
pub async fn backend_type(&self) -> HidBackendType {
|
|
self.backend_type.read().await.clone()
|
|
}
|
|
|
|
/// Keep the active capture dimensions across HID reloads and USB rebuilds.
|
|
pub async fn set_screen_resolution(&self, width: u32, height: u32) {
|
|
if width == 0 || height == 0 || width > 65535 || height > 65535 {
|
|
return;
|
|
}
|
|
{
|
|
let mut resolution = self.screen_resolution.write();
|
|
if *resolution == (width, height) {
|
|
return;
|
|
}
|
|
*resolution = (width, height);
|
|
}
|
|
if let Some(backend) = self.backend.read().await.as_ref() {
|
|
let (width, height) = *self.screen_resolution.read();
|
|
backend.set_screen_resolution(width, height);
|
|
}
|
|
}
|
|
|
|
pub async fn snapshot(&self) -> HidRuntimeState {
|
|
self.runtime_state.read().await.clone()
|
|
}
|
|
|
|
async fn ch9329_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
|
if !matches!(
|
|
*self.backend_type.read().await,
|
|
HidBackendType::Ch9329 { .. }
|
|
) {
|
|
return Err(AppError::BadRequest(
|
|
"Current HID backend is not CH9329".to_string(),
|
|
));
|
|
}
|
|
self.backend
|
|
.read()
|
|
.await
|
|
.clone()
|
|
.ok_or_else(|| AppError::BadRequest("CH9329 backend not available".to_string()))
|
|
}
|
|
|
|
pub async fn apply_ch9329_descriptor(
|
|
&self,
|
|
descriptor: &Ch9329DescriptorConfig,
|
|
) -> Result<Ch9329DescriptorState> {
|
|
self.ch9329_backend()
|
|
.await?
|
|
.apply_ch9329_descriptor(descriptor)
|
|
.await
|
|
}
|
|
|
|
pub async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
|
|
self.ch9329_backend().await?.read_ch9329_descriptor().await
|
|
}
|
|
|
|
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
|
|
info!("Reloading HID backend: {:?}", new_backend_type);
|
|
self.backend_available.store(false, Ordering::Release);
|
|
self.stop_runtime_worker().await;
|
|
|
|
if let Some(backend) = self.backend.write().await.take() {
|
|
if let Err(e) = backend.shutdown().await {
|
|
// A Bluetooth shutdown may fail to restore adapter settings. Surface
|
|
// that failure so the config transaction can roll back.
|
|
if matches!(
|
|
*self.backend_type.read().await,
|
|
HidBackendType::Bluetooth { .. }
|
|
) {
|
|
return Err(e);
|
|
}
|
|
warn!("Error shutting down old HID backend: {}", e);
|
|
}
|
|
}
|
|
|
|
let new_backend = match self
|
|
.backend_factory
|
|
.create_initialized(&new_backend_type)
|
|
.await
|
|
{
|
|
Ok(backend) => backend,
|
|
Err(error) if matches!(&new_backend_type, HidBackendType::None) => return Err(error),
|
|
Err(error) => {
|
|
warn!("Failed to initialize HID backend: {}", error);
|
|
None
|
|
}
|
|
};
|
|
|
|
{
|
|
let mut slot = self.backend.write().await;
|
|
if let Some(backend) = new_backend.as_ref() {
|
|
let (width, height) = *self.screen_resolution.read();
|
|
backend.set_screen_resolution(width, height);
|
|
}
|
|
*slot = new_backend;
|
|
}
|
|
|
|
if matches!(new_backend_type, HidBackendType::None) {
|
|
*self.backend_type.write().await = HidBackendType::None;
|
|
self.apply_runtime_state(HidRuntimeState::from_backend_type(&HidBackendType::None))
|
|
.await;
|
|
return Ok(());
|
|
}
|
|
|
|
if self.backend.read().await.is_some() {
|
|
info!("HID backend reloaded successfully: {:?}", new_backend_type);
|
|
self.start_event_worker().await;
|
|
|
|
*self.backend_type.write().await = new_backend_type.clone();
|
|
|
|
self.sync_runtime_state_from_backend().await;
|
|
self.restart_runtime_worker().await;
|
|
|
|
Ok(())
|
|
} else {
|
|
warn!("HID backend reload resulted in no active backend");
|
|
self.backend_available.store(false, Ordering::Release);
|
|
|
|
*self.backend_type.write().await = new_backend_type.clone();
|
|
|
|
let current = self.runtime_state.read().await.clone();
|
|
let error_state = HidRuntimeState::with_error(
|
|
&new_backend_type,
|
|
¤t,
|
|
"Failed to initialize HID backend",
|
|
"init_failed",
|
|
);
|
|
self.apply_runtime_state(error_state).await;
|
|
|
|
Err(AppError::Internal(
|
|
"Failed to reload HID backend".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
async fn apply_runtime_state(&self, next: HidRuntimeState) {
|
|
apply_runtime_state(&self.runtime_state, &self.events, next).await;
|
|
}
|
|
|
|
async fn sync_runtime_state_from_backend(&self) {
|
|
let backend_opt = self.backend.read().await.clone();
|
|
apply_backend_runtime_state(
|
|
&self.backend_type,
|
|
&self.runtime_state,
|
|
&self.events,
|
|
self.backend_available.as_ref(),
|
|
backend_opt.as_deref(),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
async fn start_event_worker(&self) {
|
|
let mut worker_guard = self.hid_worker.lock().await;
|
|
if worker_guard.is_some() {
|
|
return;
|
|
}
|
|
|
|
let mut rx_guard = self.hid_rx.lock().await;
|
|
let rx = match rx_guard.take() {
|
|
Some(rx) => rx,
|
|
None => return,
|
|
};
|
|
|
|
let backend = self.backend.clone();
|
|
let pending_move = self.pending_move.clone();
|
|
let pending_move_flag = self.pending_move_flag.clone();
|
|
let reset_requested = self.reset_requested.clone();
|
|
|
|
let handle = tokio::spawn(async move {
|
|
let mut rx = rx;
|
|
loop {
|
|
let event = match rx.recv().await {
|
|
Some(ev) => ev,
|
|
None => break,
|
|
};
|
|
|
|
if reset_requested.swap(false, Ordering::AcqRel) {
|
|
// A full input queue must not lose a key/button release and leave
|
|
// the host stuck. Discard the obsolete batch and send all-up.
|
|
while rx.try_recv().is_ok() {}
|
|
*pending_move.lock() = None;
|
|
pending_move_flag.store(false, Ordering::Release);
|
|
process_hid_event(QueuedHidEvent::Reset, &backend).await;
|
|
continue;
|
|
}
|
|
process_hid_event(event, &backend).await;
|
|
|
|
if pending_move_flag.swap(false, Ordering::AcqRel) {
|
|
let move_event = { pending_move.lock().take() };
|
|
if let Some(move_event) = move_event {
|
|
process_hid_event(QueuedHidEvent::Mouse(move_event), &backend).await;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
*worker_guard = Some(handle);
|
|
}
|
|
|
|
async fn restart_runtime_worker(&self) {
|
|
self.stop_runtime_worker().await;
|
|
|
|
let backend_opt = self.backend.read().await.clone();
|
|
let Some(backend) = backend_opt else {
|
|
return;
|
|
};
|
|
|
|
let mut runtime_rx = backend.subscribe_runtime();
|
|
let runtime_state = self.runtime_state.clone();
|
|
let events = self.events.clone();
|
|
let backend_available = self.backend_available.clone();
|
|
let backend_type = self.backend_type.clone();
|
|
|
|
let handle = tokio::spawn(async move {
|
|
loop {
|
|
if runtime_rx.changed().await.is_err() {
|
|
break;
|
|
}
|
|
|
|
apply_backend_runtime_state(
|
|
&backend_type,
|
|
&runtime_state,
|
|
&events,
|
|
backend_available.as_ref(),
|
|
Some(backend.as_ref()),
|
|
)
|
|
.await;
|
|
}
|
|
});
|
|
|
|
*self.runtime_worker.lock().await = Some(handle);
|
|
}
|
|
|
|
async fn stop_runtime_worker(&self) {
|
|
if let Some(handle) = self.runtime_worker.lock().await.take() {
|
|
handle.abort();
|
|
}
|
|
}
|
|
|
|
fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> {
|
|
match self.hid_tx.try_send(QueuedHidEvent::Mouse(event.clone())) {
|
|
Ok(_) => Ok(()),
|
|
Err(mpsc::error::TrySendError::Full(_)) => {
|
|
merge_pending_move(&mut self.pending_move.lock(), event);
|
|
self.pending_move_flag.store(true, Ordering::Release);
|
|
Ok(())
|
|
}
|
|
Err(mpsc::error::TrySendError::Closed(_)) => {
|
|
Err(AppError::BadRequest("HID event queue closed".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn enqueue_event(&self, event: QueuedHidEvent) -> Result<()> {
|
|
match self.hid_tx.try_send(event) {
|
|
Ok(_) => Ok(()),
|
|
Err(mpsc::error::TrySendError::Full(ev)) => {
|
|
let tx = self.hid_tx.clone();
|
|
let send_result = tokio::time::timeout(
|
|
Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS),
|
|
tx.send(ev),
|
|
)
|
|
.await;
|
|
match send_result {
|
|
Ok(Ok(())) => Ok(()),
|
|
Ok(Err(_)) => Err(AppError::BadRequest("HID event queue closed".into())),
|
|
Err(_) => {
|
|
self.reset_requested.store(true, Ordering::Release);
|
|
warn!("HID event queue full; scheduling all-input release");
|
|
Err(AppError::ServiceUnavailable(
|
|
"HID input queue full; input state will be reset".into(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
Err(mpsc::error::TrySendError::Closed(_)) => {
|
|
Err(AppError::BadRequest("HID event queue closed".to_string()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn apply_backend_runtime_state(
|
|
backend_type: &Arc<RwLock<HidBackendType>>,
|
|
runtime_state: &Arc<RwLock<HidRuntimeState>>,
|
|
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
|
backend_available: &AtomicBool,
|
|
backend: Option<&dyn HidBackend>,
|
|
) {
|
|
let backend_kind = backend_type.read().await.clone();
|
|
let next = match backend {
|
|
Some(backend) => HidRuntimeState::from_backend(&backend_kind, backend.runtime_snapshot()),
|
|
None => HidRuntimeState::from_backend_type(&backend_kind),
|
|
};
|
|
backend_available.store(next.initialized, Ordering::Release);
|
|
apply_runtime_state(runtime_state, events, next).await;
|
|
}
|
|
|
|
async fn process_hid_event(
|
|
event: QueuedHidEvent,
|
|
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
|
) {
|
|
let backend_opt = backend.read().await.clone();
|
|
let backend = match backend_opt {
|
|
Some(b) => b,
|
|
None => return,
|
|
};
|
|
|
|
let backend_for_send = backend.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
futures::executor::block_on(async move {
|
|
match event {
|
|
QueuedHidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await,
|
|
QueuedHidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await,
|
|
QueuedHidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await,
|
|
QueuedHidEvent::Reset => backend_for_send.reset().await,
|
|
}
|
|
})
|
|
})
|
|
.await;
|
|
|
|
let result = match result {
|
|
Ok(r) => r,
|
|
Err(_) => return,
|
|
};
|
|
|
|
match result {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
warn!("HID event processing failed: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn device_for_backend_type(backend_type: &HidBackendType) -> Option<String> {
|
|
match backend_type {
|
|
HidBackendType::Ch9329 { port, .. } => Some(port.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
async fn apply_runtime_state(
|
|
runtime_state: &Arc<RwLock<HidRuntimeState>>,
|
|
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
|
|
next: HidRuntimeState,
|
|
) {
|
|
let changed = {
|
|
let mut guard = runtime_state.write().await;
|
|
if *guard == next {
|
|
false
|
|
} else {
|
|
*guard = next.clone();
|
|
true
|
|
}
|
|
};
|
|
|
|
if !changed {
|
|
return;
|
|
}
|
|
|
|
if let Some(events) = events.read().await.as_ref() {
|
|
events.mark_device_info_dirty();
|
|
}
|
|
}
|
|
|
|
fn merge_pending_move(pending: &mut Option<MouseEvent>, event: MouseEvent) {
|
|
if let Some(previous) = pending {
|
|
if previous.event_type == MouseEventType::Move && event.event_type == MouseEventType::Move {
|
|
previous.x = previous.x.saturating_add(event.x).clamp(-32767, 32767);
|
|
previous.y = previous.y.saturating_add(event.y).clamp(-32767, 32767);
|
|
return;
|
|
}
|
|
}
|
|
*pending = Some(event);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod queue_tests {
|
|
use super::*;
|
|
#[tokio::test]
|
|
async fn screen_resolution_updates_backend_and_retains_valid_dimensions() {
|
|
#[cfg(unix)]
|
|
let controller = HidController::new(HidBackendType::None, None);
|
|
#[cfg(not(unix))]
|
|
let controller = HidController::new(HidBackendType::None);
|
|
let backend = Arc::new(ch9329::Ch9329Backend::new("/dev/null").unwrap());
|
|
*controller.backend.write().await = Some(backend.clone());
|
|
controller.set_screen_resolution(3840, 2160).await;
|
|
assert_eq!(
|
|
backend.runtime_snapshot().screen_resolution,
|
|
Some((3840, 2160))
|
|
);
|
|
controller.set_screen_resolution(0, 0).await;
|
|
assert_eq!(*controller.screen_resolution.read(), (3840, 2160));
|
|
controller.set_screen_resolution(1280, 720).await;
|
|
assert_eq!(
|
|
backend.runtime_snapshot().screen_resolution,
|
|
Some((1280, 720))
|
|
);
|
|
}
|
|
struct TestBackend {
|
|
pressed: Arc<AtomicBool>,
|
|
reset_done: Arc<tokio::sync::Notify>,
|
|
runtime: tokio::sync::watch::Sender<()>,
|
|
}
|
|
#[async_trait::async_trait]
|
|
impl HidBackend for TestBackend {
|
|
async fn init(&self) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
async fn send_keyboard(&self, _: KeyboardEvent) -> Result<()> {
|
|
self.pressed.store(true, Ordering::Release);
|
|
Ok(())
|
|
}
|
|
async fn send_mouse(&self, _: MouseEvent) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
async fn reset(&self) -> Result<()> {
|
|
self.pressed.store(false, Ordering::Release);
|
|
self.reset_done.notify_one();
|
|
Ok(())
|
|
}
|
|
async fn shutdown(&self) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
|
|
HidBackendRuntimeSnapshot::default()
|
|
}
|
|
fn subscribe_runtime(&self) -> tokio::sync::watch::Receiver<()> {
|
|
self.runtime.subscribe()
|
|
}
|
|
}
|
|
#[tokio::test]
|
|
async fn congested_queue_releases_host_instead_of_replaying_keydowns() {
|
|
#[cfg(unix)]
|
|
let controller = HidController::new(HidBackendType::None, None);
|
|
#[cfg(not(unix))]
|
|
let controller = HidController::new(HidBackendType::None);
|
|
let pressed = Arc::new(AtomicBool::new(true));
|
|
let done = Arc::new(tokio::sync::Notify::new());
|
|
let (runtime, _) = tokio::sync::watch::channel(());
|
|
*controller.backend.write().await = Some(Arc::new(TestBackend {
|
|
pressed: pressed.clone(),
|
|
reset_done: done.clone(),
|
|
runtime,
|
|
}));
|
|
for _ in 0..HID_EVENT_QUEUE_CAPACITY {
|
|
controller
|
|
.enqueue_event(QueuedHidEvent::Keyboard(KeyboardEvent::key_down(
|
|
CanonicalKey::KeyA,
|
|
KeyboardModifiers::default(),
|
|
)))
|
|
.await
|
|
.unwrap();
|
|
}
|
|
assert!(controller
|
|
.enqueue_event(QueuedHidEvent::Reset)
|
|
.await
|
|
.is_err());
|
|
controller.start_event_worker().await;
|
|
tokio::time::timeout(Duration::from_secs(1), done.notified())
|
|
.await
|
|
.unwrap();
|
|
assert!(!pressed.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[test]
|
|
fn relative_motion_is_accumulated_but_absolute_is_replaced() {
|
|
let mut pending = Some(MouseEvent::move_rel(100, -20));
|
|
merge_pending_move(&mut pending, MouseEvent::move_rel(80, 30));
|
|
assert_eq!(
|
|
(pending.as_ref().unwrap().x, pending.as_ref().unwrap().y),
|
|
(180, 10)
|
|
);
|
|
merge_pending_move(&mut pending, MouseEvent::move_abs(10, 20));
|
|
merge_pending_move(&mut pending, MouseEvent::move_abs(30, 40));
|
|
assert_eq!(
|
|
(pending.as_ref().unwrap().x, pending.as_ref().unwrap().y),
|
|
(30, 40)
|
|
);
|
|
}
|
|
}
|