Skip to main content

wasmtime_wasi/cli/
worker_thread_stdin.rs

1//! Handling for standard in using a worker task.
2//!
3//! Standard input is a global singleton resource for the entire program which
4//! needs special care. Currently this implementation adheres to a few
5//! constraints which make this nontrivial to implement.
6//!
7//! * Any number of guest wasm programs can read stdin. While this doesn't make
8//!   a ton of sense semantically they shouldn't block forever. Instead it's a
9//!   race to see who actually reads which parts of stdin.
10//!
11//! * Data from stdin isn't actually read unless requested. This is done to try
12//!   to be a good neighbor to others running in the process. Under the
13//!   assumption that most programs have one "thing" which reads stdin the
14//!   actual consumption of bytes is delayed until the wasm guest is dynamically
15//!   chosen to be that "thing". Before that data from stdin is not consumed to
16//!   avoid taking it from other components in the process.
17//!
18//! * Tokio's documentation indicates that "interactive stdin" is best done with
19//!   a helper thread to avoid blocking shutdown of the event loop. That's
20//!   respected here where all stdin reading happens on a blocking helper thread
21//!   that, at this time, is never shut down.
22//!
23//! This module is one that's likely to change over time though as new systems
24//! are encountered along with preexisting bugs.
25
26use crate::cli::{IsTerminal, StdinStream, stream_error_from};
27use bytes::{Bytes, BytesMut};
28use std::mem;
29use std::pin::Pin;
30use std::sync::{Condvar, Mutex, OnceLock};
31use std::task::{Context, Poll};
32use tokio::io::{self, AsyncRead, ReadBuf};
33use tokio::sync::Notify;
34use tokio::sync::futures::Notified;
35use wasmtime_wasi_io::{
36    poll::Pollable,
37    streams::{InputStream, StreamError},
38};
39
40use crate::MAX_READ_SIZE_ALLOC;
41
42// Implementation for tokio::io::Stdin
43impl IsTerminal for tokio::io::Stdin {
44    fn is_terminal(&self) -> bool {
45        std::io::stdin().is_terminal()
46    }
47}
48impl StdinStream for tokio::io::Stdin {
49    fn p2_stream(&self) -> Box<dyn InputStream> {
50        Box::new(WasiStdin)
51    }
52    fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
53        Box::new(WasiStdinAsyncRead::Ready)
54    }
55}
56
57// Implementation for std::io::Stdin
58impl IsTerminal for std::io::Stdin {
59    fn is_terminal(&self) -> bool {
60        std::io::IsTerminal::is_terminal(self)
61    }
62}
63impl StdinStream for std::io::Stdin {
64    fn p2_stream(&self) -> Box<dyn InputStream> {
65        Box::new(WasiStdin)
66    }
67    fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
68        Box::new(WasiStdinAsyncRead::Ready)
69    }
70}
71
72#[derive(Default)]
73struct GlobalStdin {
74    state: Mutex<StdinState>,
75    read_requested: Condvar,
76    read_completed: Notify,
77}
78
79#[derive(Default, Debug)]
80enum StdinState {
81    #[default]
82    ReadNotRequested,
83    ReadRequested(usize),
84    Data(BytesMut),
85    Error(std::io::Error),
86    Closed,
87}
88
89impl GlobalStdin {
90    fn get() -> &'static GlobalStdin {
91        static STDIN: OnceLock<GlobalStdin> = OnceLock::new();
92        STDIN.get_or_init(|| create())
93    }
94}
95
96fn create() -> GlobalStdin {
97    std::thread::spawn(|| {
98        let state = GlobalStdin::get();
99        loop {
100            // Wait for a read to be requested, but don't hold the lock across
101            // the blocking read.
102            let mut lock = state.state.lock().unwrap();
103            lock = state
104                .read_requested
105                .wait_while(lock, |state| !matches!(state, StdinState::ReadRequested(_)))
106                .unwrap();
107
108            // Extract the size hint from the request and cap it to `MAX_READ_SIZE_ALLOC`
109            // to avoid guest-controlled unbounded allocation.
110            // The `.max(1)` ensures a zero-length read is never misinterpreted as EOF.
111            let size_hint = match *lock {
112                StdinState::ReadRequested(size) => size.min(MAX_READ_SIZE_ALLOC).max(1),
113                _ => unreachable!(),
114            };
115            drop(lock);
116
117            let mut bytes = BytesMut::zeroed(size_hint);
118            let (new_state, done) = match read_stdin(&mut bytes) {
119                Ok(0) => (StdinState::Closed, true),
120                Ok(nbytes) => {
121                    bytes.truncate(nbytes);
122                    (StdinState::Data(bytes), false)
123                }
124                Err(e) => (StdinState::Error(e), true),
125            };
126
127            // After the blocking read completes the state should not have been
128            // tampered with.
129            debug_assert!(matches!(
130                *state.state.lock().unwrap(),
131                StdinState::ReadRequested(_)
132            ));
133            let mut lock = state.state.lock().unwrap();
134            *lock = new_state;
135            state.read_completed.notify_waiters();
136            if done {
137                break;
138            }
139        }
140    });
141
142    GlobalStdin::default()
143}
144
145// Bypass `std::io::Stdin`'s process-global buffer so that a guest request
146// cannot advance a seekable input past the requested number of bytes. Keep
147// its lock held to serialize reads with other users of stdin in this process.
148fn read_stdin(bytes: &mut [u8]) -> std::io::Result<usize> {
149    #[cfg(unix)]
150    {
151        use std::os::fd::AsFd;
152        let stdin = std::io::stdin();
153        let stdin = stdin.lock();
154        rustix::io::read(stdin.as_fd(), bytes).map_err(Into::into)
155    }
156
157    #[cfg(windows)]
158    {
159        use std::io::Read as _;
160        use std::os::windows::io::{AsRawHandle, FromRawHandle};
161
162        let stdin = std::io::stdin();
163        let mut stdin = stdin.lock();
164        if std::io::IsTerminal::is_terminal(&stdin) {
165            return stdin.read(bytes);
166        }
167
168        // SAFETY: `stdin` keeps the borrowed process handle valid for this
169        // read, and `ManuallyDrop` prevents `File` from closing the handle.
170        let mut file = std::mem::ManuallyDrop::new(unsafe {
171            std::fs::File::from_raw_handle(stdin.as_raw_handle())
172        });
173        file.read(bytes)
174    }
175
176    #[cfg(not(any(unix, windows)))]
177    {
178        use std::io::Read as _;
179        std::io::stdin().read(bytes)
180    }
181}
182
183struct WasiStdin;
184
185#[async_trait::async_trait]
186impl InputStream for WasiStdin {
187    fn read(&mut self, size: usize) -> Result<Bytes, StreamError> {
188        if size == 0 {
189            return Ok(Bytes::new());
190        }
191        let g = GlobalStdin::get();
192        let mut locked = g.state.lock().unwrap();
193        match mem::replace(&mut *locked, StdinState::ReadRequested(size)) {
194            StdinState::ReadNotRequested => {
195                g.read_requested.notify_one();
196                Ok(Bytes::new())
197            }
198            StdinState::ReadRequested(prev_size) => {
199                // Preserve the larger of the two requested sizes
200                // so the worker thread allocates an adequate buffer.
201                *locked = StdinState::ReadRequested(prev_size.max(size));
202                Ok(Bytes::new())
203            }
204            StdinState::Data(mut data) => {
205                let size = data.len().min(size);
206                let bytes = data.split_to(size);
207                *locked = if data.is_empty() {
208                    StdinState::ReadNotRequested
209                } else {
210                    StdinState::Data(data)
211                };
212                Ok(bytes.freeze())
213            }
214            StdinState::Error(e) => {
215                *locked = StdinState::Closed;
216                Err(stream_error_from(e))
217            }
218            StdinState::Closed => {
219                *locked = StdinState::Closed;
220                Err(StreamError::Closed)
221            }
222        }
223    }
224}
225
226#[async_trait::async_trait]
227impl Pollable for WasiStdin {
228    async fn ready(&mut self) {
229        let g = GlobalStdin::get();
230
231        // Scope the synchronous `state.lock()` to this block which does not
232        // `.await` inside of it.
233        let notified = {
234            let mut locked = g.state.lock().unwrap();
235            match *locked {
236                // If a read isn't requested yet, use `MAX_READ_SIZE_ALLOC`
237                // as the buffer size since `ready()` doesn't know what size
238                // will be requested by the subsequent `read()` call.
239                StdinState::ReadNotRequested => {
240                    g.read_requested.notify_one();
241                    *locked = StdinState::ReadRequested(MAX_READ_SIZE_ALLOC);
242                    g.read_completed.notified()
243                }
244                StdinState::ReadRequested(_) => g.read_completed.notified(),
245                StdinState::Data(_) | StdinState::Closed | StdinState::Error(_) => return,
246            }
247        };
248
249        notified.await;
250    }
251}
252
253enum WasiStdinAsyncRead {
254    Ready,
255    Waiting(Notified<'static>),
256}
257
258impl AsyncRead for WasiStdinAsyncRead {
259    fn poll_read(
260        mut self: Pin<&mut Self>,
261        cx: &mut Context<'_>,
262        buf: &mut ReadBuf<'_>,
263    ) -> Poll<io::Result<()>> {
264        let g = GlobalStdin::get();
265
266        // Everything below is executed under the global stdin lock. It's not
267        // going to block below so that's semantically fine. Optimization-wise
268        // it's probably possible to move this within the loop around just a
269        // small part of reading/writing the state, but that was done
270        // historically and it resulted in lost wakeups with `Notify`, so this
271        // is conservatively hoisted up here.
272        let mut locked = g.state.lock().unwrap();
273
274        // Perform everything below in a `loop` to handle the case that a read
275        // was stolen by another thread, for example, or perhaps a spurious
276        // notification to `Notified`.
277        loop {
278            // If we were previously blocked on reading a "ready" notification,
279            // wait for that notification to complete.
280            if let Some(notified) = self.as_mut().notified_future() {
281                match notified.poll(cx) {
282                    Poll::Ready(()) => self.set(WasiStdinAsyncRead::Ready),
283                    Poll::Pending => break Poll::Pending,
284                }
285            }
286
287            assert!(matches!(*self, WasiStdinAsyncRead::Ready));
288
289            // Once we're in the "ready" state then take a look at the global
290            // state of stdin.
291            match mem::replace(&mut *locked, StdinState::ReadRequested(buf.remaining())) {
292                // If data is available then drain what we can into `buf`.
293                StdinState::Data(mut data) => {
294                    let size = data.len().min(buf.remaining());
295                    let bytes = data.split_to(size);
296                    *locked = if data.is_empty() {
297                        StdinState::ReadNotRequested
298                    } else {
299                        StdinState::Data(data)
300                    };
301                    buf.put_slice(&bytes);
302                    break Poll::Ready(Ok(()));
303                }
304
305                // If stdin failed to be read then we fail with that error and
306                // transition to "closed"
307                StdinState::Error(e) => {
308                    *locked = StdinState::Closed;
309                    break Poll::Ready(Err(e));
310                }
311
312                // If stdin is closed, keep it closed.
313                StdinState::Closed => {
314                    *locked = StdinState::Closed;
315                    break Poll::Ready(Ok(()));
316                }
317
318                // For these states we indicate that a read is requested, if it
319                // wasn't previously requested, and then we transition to
320                // `Waiting` below by falling through outside this `match`.
321                StdinState::ReadNotRequested => {
322                    g.read_requested.notify_one();
323                }
324                StdinState::ReadRequested(prev_size) => {
325                    // Preserve the larger of the previous and current size hint
326                    *locked = StdinState::ReadRequested(prev_size.max(buf.remaining()));
327                }
328            }
329
330            self.set(WasiStdinAsyncRead::Waiting(g.read_completed.notified()));
331        }
332    }
333}
334
335impl WasiStdinAsyncRead {
336    fn notified_future(self: Pin<&mut Self>) -> Option<Pin<&mut Notified<'static>>> {
337        // SAFETY: this is a pin-projection from `self` to the field `Notified`
338        // internally. Given that `self` is pinned it should be safe to acquire
339        // a pinned version of the internal field.
340        unsafe {
341            match self.get_unchecked_mut() {
342                WasiStdinAsyncRead::Ready => None,
343                WasiStdinAsyncRead::Waiting(notified) => Some(Pin::new_unchecked(notified)),
344            }
345        }
346    }
347}