Skip to main content

openlogi_device/
device_io.rs

1//! Process activity gate for host HID access.
2//!
3//! The gate carries policy from a host lifecycle observer down to code that
4//! may open a HID node or send a request. It contains no host integration of
5//! its own, so the device layer remains portable and testable.
6
7use tokio::sync::watch;
8
9/// Non-blocking producer owned by the host lifecycle observer.
10#[derive(Clone)]
11pub struct DeviceIoSignal {
12    sender: watch::Sender<DeviceIoState>,
13}
14
15/// Cheaply cloneable read capability for device-I/O producers.
16#[derive(Clone)]
17pub struct DeviceIoGate {
18    receiver: watch::Receiver<DeviceIoState>,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22enum DeviceIoState {
23    Allowed,
24    Suspended,
25}
26
27/// Create an initially-open device-I/O gate.
28#[must_use]
29pub fn device_io_channel() -> (DeviceIoSignal, DeviceIoGate) {
30    let (sender, receiver) = watch::channel(DeviceIoState::Allowed);
31    (DeviceIoSignal { sender }, DeviceIoGate { receiver })
32}
33
34impl DeviceIoSignal {
35    /// Close the gate without blocking the native lifecycle callback.
36    ///
37    /// Returns whether this call changed the published state.
38    #[must_use]
39    pub fn suspend(&self) -> bool {
40        self.sender.send_if_modified(|state| {
41            if *state == DeviceIoState::Suspended {
42                return false;
43            }
44            *state = DeviceIoState::Suspended;
45            true
46        })
47    }
48
49    /// Reopen the gate after a user-visible resume.
50    ///
51    /// Returns whether this call changed the published state.
52    #[must_use]
53    pub fn resume(&self) -> bool {
54        self.sender.send_if_modified(|state| {
55            if *state == DeviceIoState::Allowed {
56                return false;
57            }
58            *state = DeviceIoState::Allowed;
59            true
60        })
61    }
62}
63
64impl DeviceIoGate {
65    /// Whether opening a node or sending a request is currently allowed.
66    #[must_use]
67    pub fn allows_io(&self) -> bool {
68        *self.receiver.borrow() == DeviceIoState::Allowed
69    }
70
71    /// Wait for the next distinct gate transition.
72    ///
73    /// Returns the new allow-state, or `None` if the lifecycle producer was
74    /// dropped.
75    pub async fn changed(&mut self) -> Option<bool> {
76        self.receiver.changed().await.ok()?;
77        Some(self.allows_io())
78    }
79
80    /// Wait until the gate is open. Returns `false` if its producer disappears
81    /// while it is closed.
82    pub async fn wait_until_allowed(&mut self) -> bool {
83        while !self.allows_io() {
84            if self.changed().await.is_none() {
85                return false;
86            }
87        }
88        true
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use std::time::Duration;
95
96    use super::device_io_channel;
97
98    #[tokio::test]
99    async fn latest_transition_is_retained_and_duplicates_coalesce() {
100        let (signal, mut gate) = device_io_channel();
101        assert!(gate.allows_io());
102
103        assert!(signal.suspend());
104        assert!(!signal.suspend());
105        gate.changed()
106            .await
107            .expect("the suspend transition should remain available");
108        assert!(!gate.allows_io());
109        tokio::time::timeout(Duration::from_millis(10), gate.changed())
110            .await
111            .expect_err("duplicate suspend publications must coalesce");
112
113        assert!(signal.resume());
114        assert!(gate.wait_until_allowed().await);
115        assert!(gate.allows_io());
116    }
117}