From 3da20c5553e39666044b0df58c4c801fbab6e4b0 Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Mon, 9 Mar 2026 20:01:21 -0500 Subject: [PATCH] Track lifecycle state of connections, seats, and devices Add per-side lifecycle state enums that match libei's variant sets. The client side defines EiConnectionState, EiDeviceState, and EiSeatState, and the server side defines EisConnectionState, EisDeviceState, and EisSeatState. Each wrapper exposes its state through a state() query and updates it as protocol events flow through the converters. The enums keep only the states each converter can actually observe. The server device tracks AwaitingReady, Paused, Resumed, Emulating, ClosedByClient, and Dead. The client device tracks Paused, Resumed, Emulating, and RemovedFromServer, since the client converter does not model client-initiated release. Pre-done phases (New on the client device, Pending on the server seat) are omitted, and the connection enum is reduced to Connected and Disconnected on both sides. State-gated validation follows as a separate incremental change. --- src/event.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++- src/request.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) 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)); }