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}