diff --git a/src/event.rs b/src/event.rs index c5c4835..6b5f33d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -24,7 +24,7 @@ use std::{ fmt, io, os::unix::io::OwnedFd, sync::{ - atomic::{AtomicU32, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, Arc, Mutex, }, }; @@ -72,6 +72,8 @@ struct ConnectionInner { handshake_resp: HandshakeResp, /// The last serial number used in an event by the server. serial: AtomicU32, + /// Set when `ei_connection.disconnected` arrives, and never cleared. + disconnected: AtomicBool, } /// High-level client-side wrapper for `ei_connection`. @@ -105,6 +107,16 @@ impl Connection { pub fn serial(&self) -> u32 { self.0.serial.load(Ordering::Relaxed) } + + /// Returns the current lifecycle state of this connection. + #[must_use] + pub fn state(&self) -> EiConnectionState { + if self.0.disconnected.load(Ordering::SeqCst) { + EiConnectionState::Disconnected + } else { + EiConnectionState::Connected + } + } } /// Utility that converts low-level protocol-level events into high-level events defined in this @@ -144,6 +156,7 @@ impl EiEventConverter { context: context.clone(), serial: AtomicU32::new(handshake_resp.serial), handshake_resp, + disconnected: AtomicBool::new(false), })), } } @@ -198,6 +211,10 @@ impl EiEventConverter { /// Handles a low-level protocol-level [`ei::Event`], possibly converting it into a high-level /// [`EiEvent`]. /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + /// /// # Errors /// /// The errors returned are protocol violations. @@ -220,6 +237,7 @@ impl EiEventConverter { proto_seat: seat, name: None, capability_map: CapabilityMap::default(), + state: Mutex::new(EiSeatState::New), }, ); } @@ -235,6 +253,7 @@ impl EiEventConverter { reason, explanation, } => { + self.connection.0.disconnected.store(true, Ordering::SeqCst); self.queue_event(EiEvent::Disconnected(Disconnected { last_serial, reason, @@ -280,6 +299,7 @@ impl EiEventConverter { .pending_seats .remove(&seat) .ok_or(EventError::SeatSetupEventAfterDone)?; + *seat.state.lock().unwrap() = EiSeatState::Done; let seat = Seat(Arc::new(seat)); self.seats.insert(seat.0.proto_seat.clone(), seat.clone()); self.queue_event(EiEvent::SeatAdded(SeatAdded { seat })); @@ -302,6 +322,7 @@ impl EiEventConverter { next_region_mapping_id: None, keymap: None, pending_events: Mutex::new(VecDeque::new()), + state: Mutex::new(EiDeviceState::Paused), }, ); } @@ -309,6 +330,7 @@ impl EiEventConverter { self.connection.update_serial(serial); self.pending_seats.remove(&seat); if let Some(seat) = self.seats.remove(&seat) { + *seat.0.state.lock().unwrap() = EiSeatState::Removed; self.queue_event(EiEvent::SeatRemoved(SeatRemoved { seat })); } } @@ -387,6 +409,7 @@ impl EiEventConverter { .devices .get(&device) .ok_or(EventError::DeviceEventBeforeDone)?; + *device.0.state.lock().unwrap() = EiDeviceState::Resumed; self.queue_event(EiEvent::DeviceResumed(DeviceResumed { device: device.clone(), serial, @@ -398,6 +421,7 @@ impl EiEventConverter { .devices .get(&device) .ok_or(EventError::DeviceEventBeforeDone)?; + *device.0.state.lock().unwrap() = EiDeviceState::Paused; self.queue_event(EiEvent::DevicePaused(DevicePaused { device: device.clone(), serial, @@ -409,6 +433,7 @@ impl EiEventConverter { .devices .get(&device) .ok_or(EventError::DeviceEventBeforeDone)?; + *device.0.state.lock().unwrap() = EiDeviceState::Emulating; self.queue_event(EiEvent::DeviceStartEmulating(DeviceStartEmulating { device: device.clone(), serial, @@ -421,6 +446,7 @@ impl EiEventConverter { .devices .get(&device) .ok_or(EventError::DeviceEventBeforeDone)?; + *device.0.state.lock().unwrap() = EiDeviceState::Resumed; self.queue_event(EiEvent::DeviceStopEmulating(DeviceStopEmulating { device: device.clone(), serial, @@ -449,6 +475,7 @@ impl EiEventConverter { self.connection.update_serial(serial); self.pending_devices.remove(&device); if let Some(device) = self.devices.remove(&device) { + *device.0.state.lock().unwrap() = EiDeviceState::RemovedFromServer; for (_, obj) in device.0.interfaces.lock().unwrap().drain() { self.device_for_interface.remove(&obj); } @@ -837,6 +864,54 @@ impl DeviceCapability { } } +/// Lifecycle state of a device, client side. +/// +/// Mirrors libei's internal `ei_device_state` so the high-level layer can track and (later) +/// validate device lifecycle the same way libei does. States the client converter never observes +/// are omitted: the pre-`done` `NEW`/`AWAITING_READY` phases (a device is only exposed after its +/// `done` event), and `REMOVED_FROM_CLIENT`/`DEAD` (the converter does not model client-initiated +/// release). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EiDeviceState { + /// Input cannot flow. A newly delivered device starts here. + Paused, + /// Input may flow, but emulation has not started. + Resumed, + /// The device is actively emulating input (between `start_emulating` and `stop_emulating`). + Emulating, + /// The server destroyed the device. + RemovedFromServer, +} + +/// Lifecycle state of a seat, client side. +/// +/// Mirrors libei's `ei_seat_state`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EiSeatState { + /// The seat has been added but its capability advertisement is not yet complete. + New, + /// The seat is fully advertised and bindable. + Done, + /// The seat has been removed. + Removed, +} + +/// Lifecycle state of a connection, client side. +/// +/// Reduced from libei's `ei_state` to the phases observable after the handshake completes, +/// since the converter only runs post-handshake. Pre-handshake validation is handled +/// separately in the handshake path. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EiConnectionState { + /// The connection is active. + Connected, + /// The connection is fully disconnected. + Disconnected, +} + /// Lookup table from [`DeviceCapability`] to a protocol capability. #[derive(Clone, Copy, PartialEq, Eq, Default)] struct CapabilityMap([u64; BitFlags::::ALL.bits_c().count_ones() as usize]); @@ -857,6 +932,7 @@ struct SeatInner { proto_seat: ei::Seat, name: Option, capability_map: CapabilityMap, + state: Mutex, } /// High-level client-side wrapper for `ei_seat`. @@ -894,6 +970,16 @@ impl Seat { self.0.name.as_deref() } + /// Returns the current lifecycle state of this seat. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] + pub fn state(&self) -> EiSeatState { + *self.0.state.lock().unwrap() + } + // TODO has_capability /// Binds to a selection of the advertised capabilities received through @@ -924,6 +1010,7 @@ struct DeviceInner { keymap: Option, // Events received for this device but not yet committed by an `ei_device.frame`. pending_events: Mutex>, + state: Mutex, } /// High-level client-side wrapper for `ei_device`. @@ -984,6 +1071,16 @@ impl Device { self.0.keymap.as_ref() } + /// Returns the current lifecycle state of this device. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] + pub fn state(&self) -> EiDeviceState { + *self.0.state.lock().unwrap() + } + /// Returns an interface proxy if it is implemented for this device. /// /// Interfaces of devices are implemented, such that there is one `ei_device` object and diff --git a/src/request.rs b/src/request.rs index 8110106..84a140e 100644 --- a/src/request.rs +++ b/src/request.rs @@ -21,6 +21,56 @@ use std::{ pub use crate::event::DeviceCapability; +/// Lifecycle state of a connection, server side. +/// +/// Reduced from libei's `eis_client_state` to the phases observable after the handshake completes, +/// since the converter only runs post-handshake. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EisConnectionState { + /// The connection is active. + Connected, + /// The connection is fully disconnected. + Disconnected, +} + +/// Lifecycle state of a device, server side. +/// +/// Mirrors libei's `eis_device_state`. The pre-`done` `NEW` phase of the C enum is omitted because +/// the converter only exposes a device after its `done` event. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EisDeviceState { + /// The device has been advertised (`done` sent) but the client has not yet sent `ready`. + AwaitingReady, + /// Input cannot flow. + Paused, + /// Input may flow, but emulation has not started. + Resumed, + /// The client is actively emulating input (between `start_emulating` and `stop_emulating`). + Emulating, + /// The client released the device. + ClosedByClient, + /// The server removed the device. + Dead, +} + +/// Lifecycle state of a seat, server side. +/// +/// Mirrors libei's `eis_seat_state`. The pre-`done` `PENDING` phase is omitted because the +/// converter only exposes a seat after its `done` event, and libeis's internal +/// `REMOVED_INTERNALLY` is folded into `Removed`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum EisSeatState { + /// The seat has been advertised and is ready to be bound. + Added, + /// The client has bound capabilities on the seat. + Bound, + /// The seat has been removed. + Removed, +} + // For compatability, defined the same way as libei const EIS_MAX_TOUCHES: usize = 16; @@ -123,6 +173,16 @@ impl Connection { self.0.context.flush() } + /// Returns the current lifecycle state of this connection. + #[must_use] + pub fn state(&self) -> EisConnectionState { + if self.0.disconnected.load(Ordering::SeqCst) { + EisConnectionState::Disconnected + } else { + EisConnectionState::Connected + } + } + /// Returns the context type of this this connection. /// /// That is — whether the client emulates input events via requests or receives @@ -224,6 +284,7 @@ impl Connection { name: name.map(std::borrow::ToOwned::to_owned), handle: Arc::downgrade(&self.0), advertised_capabilities: capabilities, + state: Mutex::new(EisSeatState::Added), })); self.0 .seats @@ -426,6 +487,7 @@ impl EisRequestConverter { return Err(RequestError::InvalidCapabilities.into()); } + *seat.0.state.lock().unwrap() = EisSeatState::Bound; self.queue_request(EisRequest::Bind(Bind { seat, capabilities })); return Ok(()); } @@ -464,12 +526,16 @@ impl EisRequestConverter { }; match request { eis::device::Request::Release => { + // Matches libeis, where eis_device_closed_by_client() updates the + // state before the server application calls eis_device_remove(). + *device.0.state.lock().unwrap() = EisDeviceState::ClosedByClient; self.queue_request(EisRequest::DeviceClosed(DeviceClosed { device })); } eis::device::Request::StartEmulating { last_serial, sequence, } => { + *device.0.state.lock().unwrap() = EisDeviceState::Emulating; self.queue_request(EisRequest::DeviceStartEmulating(DeviceStartEmulating { device, last_serial, @@ -477,6 +543,7 @@ impl EisRequestConverter { })); } eis::device::Request::StopEmulating { last_serial } => { + *device.0.state.lock().unwrap() = EisDeviceState::Resumed; self.queue_request(EisRequest::DeviceStopEmulating(DeviceStopEmulating { device, last_serial, @@ -493,6 +560,7 @@ impl EisRequestConverter { })); } eis::device::Request::Ready => { + *device.0.state.lock().unwrap() = EisDeviceState::Paused; self.queue_request(EisRequest::Ready(Ready { device })); } } @@ -790,6 +858,7 @@ struct SeatInner { name: Option, handle: Weak, advertised_capabilities: BitFlags, + state: Mutex, } /// High-level server-side wrapper for `ei_seat`. @@ -815,6 +884,16 @@ impl Seat { &self.0.seat } + /// Returns the current lifecycle state of this seat. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] + pub fn state(&self) -> EisSeatState { + *self.0.state.lock().unwrap() + } + // builder pattern? /// Adds a device to the connection. /// @@ -881,6 +960,7 @@ impl Seat { name: name.map(ToOwned::to_owned), interfaces: Mutex::new(interfaces), handle: self.0.handle.clone(), + state: Mutex::new(EisDeviceState::AwaitingReady), down_touch_ids: Mutex::new(HashSet::new()), pending_requests: Mutex::new(VecDeque::new()), })); @@ -913,6 +993,7 @@ impl Seat { /// /// Will panic if an internal Mutex is poisoned. pub fn remove(&self) { + *self.0.state.lock().unwrap() = EisSeatState::Removed; if let Some(handle) = self.0.handle.upgrade().map(Connection) { let devices = handle .0 @@ -1000,6 +1081,7 @@ struct DeviceInner { name: Option, interfaces: Mutex>, handle: Weak, + state: Mutex, // Applicable only for touch devices down_touch_ids: Mutex>, // Requests received for this device but not yet committed by an `ei_device.frame`. @@ -1073,6 +1155,16 @@ impl Device { .contains_key(capability.interface_name()) } + /// Returns the current lifecycle state of this device. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. + #[must_use] + pub fn state(&self) -> EisDeviceState { + *self.0.state.lock().unwrap() + } + /// Removes this device and associated interfaces from the connection. /// /// After removal, [`interface`](Self::interface) returns `None` and @@ -1085,6 +1177,7 @@ impl Device { // Discard requests still waiting for a frame self.0.pending_requests.lock().unwrap().clear(); + *self.0.state.lock().unwrap() = EisDeviceState::Dead; if let Some(handle) = self.0.handle.upgrade().map(Connection) { let interfaces: Vec<_> = self .0 @@ -1112,7 +1205,12 @@ impl Device { /// Notifies to the client that, depending on the context type, it may request to start emulating or receiving input events. A newly advertised device is in the [`paused`](Self::paused) state. /// /// See [`eis::Device::resumed`] for documentation from the protocol specification. + /// + /// # Panics + /// + /// Will panic if an internal Mutex is poisoned. pub fn resumed(&self) { + *self.0.state.lock().unwrap() = EisDeviceState::Resumed; if let Some(handle) = self.0.handle.upgrade().map(Connection) { handle.with_next_serial(|serial| self.device().resumed(serial)); } @@ -1127,6 +1225,7 @@ impl Device { /// /// Will panic if an internal Mutex is poisoned. pub fn paused(&self) { + *self.0.state.lock().unwrap() = EisDeviceState::Paused; if let Some(handle) = self.0.handle.upgrade().map(Connection) { handle.with_next_serial(|serial| self.device().paused(serial)); }