Skip to main content

uncurses/event/
stream.rs

1//! Thread-backed asynchronous stream over a shared [`EventSource`].
2//!
3//! ## Purpose
4//!
5//! [`EventStream`] adapts a blocking [`EventSource`] into a
6//! [`futures_core::Stream`] of `io::Result<Event>`. It preserves the same
7//! decoder and queue semantics as synchronous reading while avoiding blocking
8//! the async task that polls the stream.
9//!
10//! ```text
11//! async task poll_next ─┬─ try lock + drain ready events ──▶ Poll::Ready
12//!                       └─ arm helper thread ─┬─ brief lock: read decode deadline
13//!                                             └─ block lock-free on cloned poller ──▶ wake task
14//! drop stream ──▶ source Waker ──▶ helper exits
15//! ```
16//!
17//! ## Key types
18//!
19//! * [`EventStream`] owns or shares an `Arc<Mutex<EventSource<_>>>`, a cloned
20//!   `Arc<dyn Poller>`, a source [`Waker`], and a helper thread channel.
21//! * A private `Wait` value tells the helper whether a wait is in flight and
22//!   whether stream drop requested shutdown.
23//!
24//! ## Lifecycle
25//!
26//! Use [`EventSource::into_stream`] when the stream should be the sole owner of
27//! the source. Use [`EventStream::from_shared`] when synchronous code keeps an
28//! `Arc<Mutex<_>>` clone. Dropping the stream wakes the helper so it can stop;
29//! the underlying shared source is not closed or drained.
30//!
31//! ## Coexistence caveats
32//!
33//! Sharing one source between a live stream and synchronous readers is
34//! supported but best-effort. The helper waits **lock-free** on a cloned poller
35//! (it locks the source only briefly to read the decode deadline, then releases
36//! it), so a synchronous reader, [`Screen::render`], or teardown can take the
37//! source lock while the stream is parked. Events go to whichever consumer
38//! drains first and are not broadcast. Read errors surface once as
39//! `Some(Err(_))`; after that the stream fuses to `None`.
40//!
41//! [`Screen::render`]: crate::screen::Screen::render
42//!
43//! [`futures_core::Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
44use std::io;
45use std::pin::Pin;
46use std::sync::atomic::{AtomicBool, Ordering};
47use std::sync::mpsc::{self, Receiver, SyncSender};
48use std::sync::{Arc, Mutex, TryLockError};
49use std::task::{Context, Poll, Waker as TaskWaker};
50use std::time::Duration;
51
52use super::Event;
53use super::poll::Poller;
54use super::source::{EventSource, Input, READY_SLOTS, Waker};
55
56/// A readiness wait requested by the polling task: block until the source
57/// signals input (or a decode deadline elapses), then wake the task's
58/// latest registered waker.
59struct Wait {
60    /// Cleared by the helper just before waking, so the task can request a
61    /// fresh wait on its next poll. Guards against queueing more than one
62    /// wait at a time.
63    dispatched: Arc<AtomicBool>,
64    /// Set on drop to break the helper out of its blocking wait and end the
65    /// thread instead of re-arming.
66    shutdown: Arc<AtomicBool>,
67}
68
69/// A thread-backed [`futures_core::Stream`] of `io::Result<Event>` over a
70/// shared [`EventSource`].
71///
72/// Build one with [`EventSource::into_stream`] (sole owner) or
73/// [`EventStream::from_shared`] (sharing a source kept elsewhere). A helper
74/// thread blocks in [`EventSource::poll`], which reads and decodes input
75/// into the source's queue, then wakes the polling task; the task drains
76/// queued events (and may itself decode via a non-blocking poll). The stream
77/// yields `Some(Ok(event))` per decoded event and, once, `Some(Err(_))` on a
78/// read error or end-of-input, then fuses to `None`. Dropping it ends the
79/// helper thread; the shared source is left intact for any other holder.
80pub struct EventStream<I: Input> {
81    source: Arc<Mutex<EventSource<I>>>,
82    /// Cloned source waker, used on drop to break the helper's blocking
83    /// wait.
84    waker: Waker,
85    /// Set while a readiness wait is queued, so only one is in flight.
86    dispatched: Arc<AtomicBool>,
87    /// Set on drop to tell the in-flight wait (if any) to end the helper.
88    shutdown: Arc<AtomicBool>,
89    /// Latest task waker, refreshed on every pending poll so the helper
90    /// always wakes the current one even if a wait is already in flight
91    /// (the task's waker may change between polls).
92    task_waker: Arc<Mutex<Option<TaskWaker>>>,
93    /// Hands a readiness wait to the helper thread.
94    waits: SyncSender<Wait>,
95    /// Latched once a read error or end-of-input has been surfaced, after
96    /// which the stream yields `None`.
97    done: bool,
98}
99
100impl<I> EventSource<I>
101where
102    I: Input + 'static,
103{
104    /// Convert this source into a thread-backed [`EventStream`].
105    ///
106    /// The source is wrapped in `Arc<Mutex<_>>` owned solely by the stream.
107    /// To keep reading the source synchronously alongside the stream, build
108    /// the `Arc<Mutex<_>>` yourself and use [`EventStream::from_shared`].
109    pub fn into_stream(self) -> EventStream<I> {
110        EventStream::from_shared(Arc::new(Mutex::new(self)))
111    }
112}
113
114impl<I> EventStream<I>
115where
116    I: Input + 'static,
117{
118    /// Build a stream over a source shared via `Arc<Mutex<_>>`.
119    ///
120    /// The caller may keep its own clone of the `Arc` to read the source
121    /// synchronously while the stream is live (see the coexistence caveats
122    /// on this module).
123    pub fn from_shared(source: Arc<Mutex<EventSource<I>>>) -> Self {
124        let (waker, poller) = {
125            let src = source.lock().unwrap();
126            (src.waker(), src.poller())
127        };
128        let (waits, rx) = mpsc::sync_channel::<Wait>(1);
129        let task_waker: Arc<Mutex<Option<TaskWaker>>> = Arc::new(Mutex::new(None));
130        let wait_source = Arc::clone(&source);
131        let wait_waker = Arc::clone(&task_waker);
132        std::thread::Builder::new()
133            .name("uncurses-event-waiter".to_string())
134            .spawn(move || waiter_loop(wait_source, poller, wait_waker, rx))
135            .expect("spawn event waiter thread");
136        Self {
137            source,
138            waker,
139            dispatched: Arc::new(AtomicBool::new(false)),
140            shutdown: Arc::new(AtomicBool::new(false)),
141            task_waker,
142            waits,
143            done: false,
144        }
145    }
146}
147
148impl<I> EventStream<I>
149where
150    I: Input,
151{
152    /// Record the current task waker and, if none is in flight, queue a
153    /// single readiness wait so the helper wakes the task when input
154    /// arrives or a decode deadline elapses.
155    fn arm(&self, cx: &Context<'_>) {
156        // Always refresh the waker, even when a wait is already in flight:
157        // the helper wakes whatever is latest, so a changed waker is never
158        // lost.
159        *self.task_waker.lock().unwrap() = Some(cx.waker().clone());
160        if !self
161            .dispatched
162            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
163            .unwrap_or_else(|prev| prev)
164        {
165            self.shutdown.store(false, Ordering::SeqCst);
166            let _ = self.waits.send(Wait {
167                dispatched: Arc::clone(&self.dispatched),
168                shutdown: Arc::clone(&self.shutdown),
169            });
170        }
171    }
172}
173
174impl<I: Input> Drop for EventStream<I> {
175    fn drop(&mut self) {
176        // Tell the in-flight wait to end the helper, then break it out of
177        // its blocking readiness wait. The `waits` channel closes as this
178        // value drops, so the helper's `recv` then returns and the thread
179        // exits. The shared source itself is untouched.
180        self.shutdown.store(true, Ordering::SeqCst);
181        let _ = self.waker.wake();
182    }
183}
184
185impl<I: Input> futures_core::Stream for EventStream<I> {
186    type Item = io::Result<Event>;
187
188    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
189        let this = self.get_mut();
190        if this.done {
191            return Poll::Ready(None);
192        }
193        // Try to decode on this task without blocking. If the helper holds
194        // the lock (parked in its brief deadline peek), fall through to arm a
195        // wait and stay pending rather than block here.
196        match this.source.try_lock() {
197            Ok(mut src) => {
198                if let Some(ev) = src.try_read() {
199                    return Poll::Ready(Some(Ok(ev)));
200                }
201                // Only drive I/O when no waiter is in flight. A dispatched
202                // waiter owns the next readiness wait and already captured the
203                // decode deadline; draining here could consume input (and set
204                // a fresh ESC/paste deadline) that the parked waiter's timeout
205                // won't honor, hanging a lone Esc until unrelated input. When
206                // a waiter is in flight the input stays level-ready, so the
207                // waiter wakes on it and the next poll (with `dispatched`
208                // cleared) drains and re-arms with the new deadline.
209                if !this.dispatched.load(Ordering::SeqCst) {
210                    match src.poll(Some(Duration::ZERO)) {
211                        Ok(_) => {
212                            if let Some(ev) = src.try_read() {
213                                return Poll::Ready(Some(Ok(ev)));
214                            }
215                        }
216                        // A stray wake is not an error; just stay pending.
217                        Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
218                        // A read error (including end-of-input) surfaces once,
219                        // then the stream fuses.
220                        Err(e) => {
221                            this.done = true;
222                            return Poll::Ready(Some(Err(e)));
223                        }
224                    }
225                }
226            }
227            Err(TryLockError::WouldBlock) => {}
228            Err(TryLockError::Poisoned(_)) => {
229                this.done = true;
230                return Poll::Ready(Some(Err(io::Error::other("event source mutex poisoned"))));
231            }
232        }
233        this.arm(cx);
234        Poll::Pending
235    }
236}
237
238fn waiter_loop<I: Input>(
239    source: Arc<Mutex<EventSource<I>>>,
240    poller: Arc<dyn Poller>,
241    task_waker: Arc<Mutex<Option<TaskWaker>>>,
242    waits: Receiver<Wait>,
243) {
244    while let Ok(wait) = waits.recv() {
245        // Wait for readiness WITHOUT holding the source mutex, so the owner
246        // (render, teardown, capability application) can lock the source
247        // freely while this thread is parked. The poller is level-triggered
248        // and `poll` takes `&self`, so a concurrent readiness check here and a
249        // later drain by the task both observe the same readiness. One wait
250        // per dispatched request; the task re-arms for the next.
251        if !wait.shutdown.load(Ordering::SeqCst) {
252            // Briefly lock only to read the nearest ESC/paste decode deadline.
253            // The guard is dropped at the end of this `map`, before the
254            // blocking wait below, so the source stays lockable while parked.
255            // Without honoring the deadline, a buffered partial escape with no
256            // further input would never wake the task and a lone Esc would
257            // hang. On a poisoned lock, skip the wait and let the task surface
258            // the poison on its next poll.
259            if let Ok(timeout) = source.lock().map(|src| src.effective_timeout(None)) {
260                let mut ready = [false; READY_SLOTS];
261                // Any return (readiness, elapsed deadline, wake, or error)
262                // hands back to the task, which drains and decodes under its
263                // own `try_lock`; stray wakes are harmless.
264                let _ = poller.poll(&mut ready, timeout);
265            }
266        }
267        wait.dispatched.store(false, Ordering::SeqCst);
268        if let Some(waker) = task_waker.lock().unwrap().as_ref() {
269            waker.wake_by_ref();
270        }
271    }
272}
273
274#[cfg(all(test, unix))]
275mod tests {
276    use std::fs::File;
277    use std::os::fd::FromRawFd;
278    use std::os::unix::io::AsRawFd;
279    use std::pin::Pin;
280    use std::sync::Arc;
281    use std::sync::atomic::{AtomicBool, Ordering};
282    use std::task::{Context, Poll, Wake, Waker};
283    use std::time::Duration;
284
285    use futures_core::Stream;
286
287    use super::*;
288    use crate::event::KeyCode;
289
290    fn make_pipe() -> (File, File) {
291        let mut fds = [0i32; 2];
292        let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
293        assert_eq!(rc, 0, "pipe(2) failed");
294        // SAFETY: pipe(2) just produced two fresh, owned fds.
295        let rx = unsafe { File::from_raw_fd(fds[0]) };
296        let tx = unsafe { File::from_raw_fd(fds[1]) };
297        (rx, tx)
298    }
299
300    fn write_bytes(f: &File, bytes: &[u8]) {
301        let n = unsafe { libc::write(f.as_raw_fd(), bytes.as_ptr() as *const _, bytes.len()) };
302        assert_eq!(n, bytes.len() as isize);
303    }
304
305    /// Minimal waker that flips a flag when woken, so a parked test thread
306    /// can re-poll the stream without an async runtime.
307    struct FlagWaker(AtomicBool);
308
309    impl Wake for FlagWaker {
310        fn wake(self: Arc<Self>) {
311            self.0.store(true, Ordering::SeqCst);
312        }
313        fn wake_by_ref(self: &Arc<Self>) {
314            self.0.store(true, Ordering::SeqCst);
315        }
316    }
317
318    /// Drive `poll_next` to its next `Ready`, parking briefly between
319    /// pending polls.
320    fn next_blocking<I: Input>(stream: &mut EventStream<I>) -> Option<io::Result<Event>> {
321        let flag = Arc::new(FlagWaker(AtomicBool::new(false)));
322        let waker = Waker::from(Arc::clone(&flag));
323        loop {
324            let mut cx = Context::from_waker(&waker);
325            match Pin::new(&mut *stream).poll_next(&mut cx) {
326                Poll::Ready(item) => return item,
327                Poll::Pending => {
328                    while !flag.0.swap(false, Ordering::SeqCst) {
329                        std::thread::sleep(Duration::from_millis(1));
330                    }
331                }
332            }
333        }
334    }
335
336    #[test]
337    fn reads_events_in_order() {
338        let (rx, tx) = make_pipe();
339        let mut stream = EventSource::new(rx).unwrap().into_stream();
340        write_bytes(&tx, b"ab");
341        let a = next_blocking(&mut stream).unwrap().unwrap();
342        let b = next_blocking(&mut stream).unwrap().unwrap();
343        assert!(matches!(a, Event::KeyPress(k) if k.code == KeyCode::Char('a')));
344        assert!(matches!(b, Event::KeyPress(k) if k.code == KeyCode::Char('b')));
345    }
346
347    #[test]
348    fn waits_then_reads_late_input() {
349        let (rx, tx) = make_pipe();
350        let mut stream = EventSource::new(rx).unwrap().into_stream();
351        // Input arrives after the first poll parks, so the wake path runs.
352        std::thread::spawn(move || {
353            std::thread::sleep(Duration::from_millis(20));
354            write_bytes(&tx, b"z");
355        });
356        let z = next_blocking(&mut stream).unwrap().unwrap();
357        assert!(matches!(z, Event::KeyPress(k) if k.code == KeyCode::Char('z')));
358    }
359
360    #[test]
361    fn surfaces_error_then_fuses_on_input_eof() {
362        let (rx, tx) = make_pipe();
363        let mut stream = EventSource::new(rx).unwrap().into_stream();
364        // Closing the write end makes the read end report EOF, which the
365        // stream surfaces once as an error item and then fuses to `None`.
366        std::thread::spawn(move || {
367            std::thread::sleep(Duration::from_millis(20));
368            drop(tx);
369        });
370        let item = next_blocking(&mut stream).unwrap();
371        assert!(
372            matches!(&item, Err(e) if e.kind() == io::ErrorKind::UnexpectedEof),
373            "expected UnexpectedEof, got {:?}",
374            item
375        );
376        assert!(
377            next_blocking(&mut stream).is_none(),
378            "stream should fuse to None after the error"
379        );
380    }
381
382    #[test]
383    fn sync_reads_coexist_with_a_live_stream() {
384        // A second handle to the same shared source can read synchronously
385        // while a stream is live; neither path deadlocks.
386        let (rx, tx) = make_pipe();
387        let shared = Arc::new(Mutex::new(EventSource::new(rx).unwrap()));
388        let mut stream = EventStream::from_shared(Arc::clone(&shared));
389        write_bytes(&tx, b"a");
390        // The synchronous side can take the lock and read.
391        let ev = shared.lock().unwrap().read().unwrap();
392        assert!(matches!(ev, Event::KeyPress(k) if k.code == KeyCode::Char('a')));
393        // The stream still functions for subsequent input.
394        write_bytes(&tx, b"b");
395        let b = next_blocking(&mut stream).unwrap().unwrap();
396        assert!(matches!(b, Event::KeyPress(k) if k.code == KeyCode::Char('b')));
397    }
398
399    #[test]
400    fn source_lockable_while_stream_parked() {
401        // Path 2: the waiter must not hold the source lock while parked, so
402        // the owner (render, teardown, capability application) can lock the
403        // source even with no input pending. Under the old lock-while-parked
404        // design this would block until input arrived.
405        let (rx, tx) = make_pipe();
406        let shared = Arc::new(Mutex::new(EventSource::new(rx).unwrap()));
407        let mut stream = EventStream::from_shared(Arc::clone(&shared));
408
409        // Poll once with no input: the stream parks and the waiter thread
410        // enters its blocking, lock-free readiness wait.
411        let flag = Arc::new(FlagWaker(AtomicBool::new(false)));
412        let waker = Waker::from(Arc::clone(&flag));
413        let mut cx = Context::from_waker(&waker);
414        assert!(matches!(
415            Pin::new(&mut stream).poll_next(&mut cx),
416            Poll::Pending
417        ));
418        // Give the waiter time to pass its brief deadline peek and reach the
419        // blocking poll.
420        std::thread::sleep(Duration::from_millis(20));
421
422        assert!(
423            shared.try_lock().is_ok(),
424            "source lock is held by the parked waiter (deadlock risk)"
425        );
426
427        // The stream still delivers input afterwards.
428        write_bytes(&tx, b"q");
429        let q = next_blocking(&mut stream).unwrap().unwrap();
430        assert!(matches!(q, Event::KeyPress(k) if k.code == KeyCode::Char('q')));
431    }
432
433    #[test]
434    fn lone_esc_resolves_through_the_stream() {
435        // A bare Esc has no follow-up bytes, so it only resolves once its
436        // decode deadline elapses. The stream must honor that deadline: the
437        // waiter wakes on the elapsed timeout and the next poll drains the
438        // resolved key. Guards the `dispatched`-gated drain path.
439        let (rx, tx) = make_pipe();
440        let mut stream = EventSource::new(rx).unwrap().into_stream();
441        write_bytes(&tx, b"\x1b");
442        let esc = next_blocking(&mut stream).unwrap().unwrap();
443        assert!(matches!(esc, Event::KeyPress(k) if k.code == KeyCode::Escape));
444    }
445
446    fn _assert_send_sync<T: Send + Sync>() {}
447    #[test]
448    fn stream_is_send_sync() {
449        _assert_send_sync::<EventStream<File>>();
450    }
451}