wasmtime_wasi/cli/locked_async.rs
1use crate::cli::{IsTerminal, StdinStream, StdoutStream};
2use crate::p2;
3use bytes::Bytes;
4use std::mem;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context, Poll, ready};
8use tokio::io::{self, AsyncRead, AsyncWrite};
9use tokio::sync::{Mutex, OwnedMutexGuard};
10use wasmtime_wasi_io::streams::{InputStream, OutputStream};
11
12/// An impl of [`StdinStream`] built on top of [`AsyncRead`].
13//
14// Note the usage of `tokio::sync::Mutex` here as opposed to a
15// `std::sync::Mutex`. This is intentionally done to implement the `Pollable`
16// variant of this trait. Note that in doing so we're left with the quandry of
17// how to implement methods of `InputStream` since those methods are not
18// `async`. They're currently implemented with `try_lock`, which then raises the
19// question of what to do on contention. Currently traps are returned.
20//
21// Why should it be ok to return a trap? In general concurrency/contention
22// shouldn't return a trap since it should be able to happen normally. The
23// current assumption, though, is that WASI stdin/stdout streams are special
24// enough that the contention case should never come up in practice. Currently
25// in WASI there is no actually concurrency, there's just the items in a single
26// `Store` and that store owns all of its I/O in a single Tokio task. There's no
27// means to actually spawn multiple Tokio tasks that use the same store. This
28// means at the very least that there's zero parallelism. Due to the lack of
29// multiple tasks that also means that there's no concurrency either.
30//
31// This `AsyncStdinStream` wrapper is only intended to be used by the WASI
32// bindings themselves. It's possible for the host to take this and work with it
33// on its own task, but that's niche enough it's not designed for.
34//
35// Overall that means that the guest is either calling `Pollable` or
36// `InputStream` methods. This means that there should never be contention
37// between the two at this time. This may all change in the future with WASI
38// 0.3, but perhaps we'll have a better story for stdio at that time (see the
39// doc block on the `OutputStream` impl below)
40pub struct AsyncStdinStream(Arc<Mutex<p2::pipe::AsyncReadStream>>);
41
42impl AsyncStdinStream {
43 pub fn new(s: impl AsyncRead + Send + Sync + 'static) -> Self {
44 Self(Arc::new(Mutex::new(p2::pipe::AsyncReadStream::new(s))))
45 }
46}
47
48impl StdinStream for AsyncStdinStream {
49 fn p2_stream(&self) -> Box<dyn InputStream> {
50 Box::new(Self(self.0.clone()))
51 }
52 fn async_stream(&self) -> Box<dyn AsyncRead + Send + Sync> {
53 Box::new(StdioHandle::Ready(self.0.clone()))
54 }
55}
56
57impl IsTerminal for AsyncStdinStream {
58 fn is_terminal(&self) -> bool {
59 false
60 }
61}
62
63#[async_trait::async_trait]
64impl InputStream for AsyncStdinStream {
65 fn read(&mut self, size: usize) -> Result<bytes::Bytes, p2::StreamError> {
66 match self.0.try_lock() {
67 Ok(mut stream) => stream.read(size),
68 Err(_) => Err(p2::StreamError::trap("concurrent reads are not supported")),
69 }
70 }
71 fn skip(&mut self, size: usize) -> Result<usize, p2::StreamError> {
72 match self.0.try_lock() {
73 Ok(mut stream) => stream.skip(size),
74 Err(_) => Err(p2::StreamError::trap("concurrent skips are not supported")),
75 }
76 }
77 async fn cancel(&mut self) {
78 // Cancel the inner stream if we're the last reference to it:
79 if let Some(mutex) = Arc::get_mut(&mut self.0) {
80 match mutex.try_lock() {
81 Ok(mut stream) => stream.cancel().await,
82 Err(_) => {}
83 }
84 }
85 }
86}
87
88#[async_trait::async_trait]
89impl p2::Pollable for AsyncStdinStream {
90 async fn ready(&mut self) {
91 self.0.lock().await.ready().await
92 }
93}
94
95impl AsyncRead for StdioHandle<p2::pipe::AsyncReadStream> {
96 fn poll_read(
97 mut self: Pin<&mut Self>,
98 cx: &mut Context<'_>,
99 buf: &mut io::ReadBuf<'_>,
100 ) -> Poll<io::Result<()>> {
101 match ready!(self.as_mut().poll(cx, |cx, g| {
102 ready!(g.poll_ready(cx));
103 Poll::Ready(g.read(buf.remaining()))
104 })) {
105 Some(Ok(bytes)) => {
106 buf.put_slice(&bytes);
107 Poll::Ready(Ok(()))
108 }
109 Some(Err(e)) => Poll::Ready(Err(e)),
110 // If the guard can't be acquired that means that this stream is
111 // closed, so return that we're ready without filling in data.
112 None => Poll::Ready(Ok(())),
113 }
114 }
115}
116
117/// A wrapper of [`crate::p2::pipe::AsyncWriteStream`] that implements
118/// [`StdoutStream`]. Note that the [`OutputStream`] impl for this is not
119/// correct when used for interleaved async IO.
120//
121// Note that the use of `tokio::sync::Mutex` here is intentional, in addition to
122// the `try_lock()` calls below in the implementation of `OutputStream`. For
123// more information see the documentation on `AsyncStdinStream`.
124pub struct AsyncStdoutStream(Arc<Mutex<p2::pipe::AsyncWriteStream>>);
125
126impl AsyncStdoutStream {
127 pub fn new(budget: usize, s: impl AsyncWrite + Send + Sync + 'static) -> Self {
128 Self(Arc::new(Mutex::new(p2::pipe::AsyncWriteStream::new(
129 budget, s,
130 ))))
131 }
132}
133
134impl StdoutStream for AsyncStdoutStream {
135 fn p2_stream(&self) -> Box<dyn OutputStream> {
136 Box::new(Self(self.0.clone()))
137 }
138 fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
139 Box::new(StdoutHandle {
140 handle: StdioHandle::Ready(self.0.clone()),
141 flushing: false,
142 })
143 }
144}
145
146impl IsTerminal for AsyncStdoutStream {
147 fn is_terminal(&self) -> bool {
148 false
149 }
150}
151
152// This implementation is known to be bogus. All check-writes and writes are
153// directed at the same underlying stream. The check-write/write protocol does
154// require the size returned by a check-write to be accepted by write, even if
155// other side-effects happen between those calls, and this implementation
156// permits another view (created by StdoutStream::stream()) of the same
157// underlying stream to accept a write which will invalidate a prior
158// check-write of another view.
159// Ultimately, the Std{in,out}Stream::stream() methods exist because many
160// different places in a linked component (which may itself contain many
161// modules) may need to access stdio without any coordination to keep those
162// accesses all using pointing to the same resource. So, we allow many
163// resources to be created. We have the reasonable expectation that programs
164// won't attempt to interleave async IO from these disparate uses of stdio.
165// If that expectation doesn't turn out to be true, and you find yourself at
166// this comment to correct it: sorry about that.
167#[async_trait::async_trait]
168impl OutputStream for AsyncStdoutStream {
169 fn check_write(&mut self) -> Result<usize, p2::StreamError> {
170 match self.0.try_lock() {
171 Ok(mut stream) => stream.check_write(),
172 Err(_) => Err(p2::StreamError::trap("concurrent writes are not supported")),
173 }
174 }
175 fn write(&mut self, bytes: Bytes) -> Result<(), p2::StreamError> {
176 match self.0.try_lock() {
177 Ok(mut stream) => stream.write(bytes),
178 Err(_) => Err(p2::StreamError::trap("concurrent writes not supported yet")),
179 }
180 }
181 fn flush(&mut self) -> Result<(), p2::StreamError> {
182 match self.0.try_lock() {
183 Ok(mut stream) => stream.flush(),
184 Err(_) => Err(p2::StreamError::trap(
185 "concurrent flushes not supported yet",
186 )),
187 }
188 }
189 async fn cancel(&mut self) {
190 // Cancel the inner stream if we're the last reference to it:
191 if let Some(mutex) = Arc::get_mut(&mut self.0) {
192 match mutex.try_lock() {
193 Ok(mut stream) => stream.cancel().await,
194 Err(_) => {}
195 }
196 }
197 }
198}
199
200#[async_trait::async_trait]
201impl p2::Pollable for AsyncStdoutStream {
202 async fn ready(&mut self) {
203 self.0.lock().await.ready().await
204 }
205}
206
207struct StdoutHandle {
208 handle: StdioHandle<p2::pipe::AsyncWriteStream>,
209 flushing: bool,
210}
211
212impl AsyncWrite for StdoutHandle {
213 fn poll_write(
214 mut self: Pin<&mut Self>,
215 cx: &mut Context<'_>,
216 buf: &[u8],
217 ) -> Poll<io::Result<usize>> {
218 if buf.is_empty() {
219 return Poll::Ready(Ok(0));
220 }
221 match ready!(Pin::new(&mut self.handle).poll(cx, |cx, i| {
222 // Use `check_write` to see if the stream isn't yet ready (a 0
223 // return value) and also how much it's willing to accept should it
224 // be nonzero.
225 loop {
226 let amt = match i.check_write() {
227 Ok(amt) => amt.min(buf.len()),
228 Err(e) => return Poll::Ready(Err(e)),
229 };
230 if amt > 0 {
231 return Poll::Ready(i.write(Bytes::copy_from_slice(&buf[..amt])).map(|()| amt));
232 }
233 ready!(i.poll_ready(cx));
234 }
235 })) {
236 Some(result) => Poll::Ready(result),
237 None => Poll::Ready(Ok(0)),
238 }
239 }
240 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
241 // First request a flush of the underlying stream.
242 if !self.flushing {
243 match ready!(Pin::new(&mut self.handle).poll(cx, |cx, i| {
244 ready!(i.poll_ready(cx));
245 Poll::Ready(i.flush())
246 })) {
247 Some(Ok(())) => self.flushing = true,
248 Some(Err(e)) => return Poll::Ready(Err(e)),
249 None => return Poll::Ready(Ok(())),
250 }
251 }
252
253 // Next wait for the stream to become ready again, meaning that the
254 // flush has completed. Here `check_write` is called again to pick up
255 // any error which happened along the way.
256 let result = ready!(Pin::new(&mut self.handle).poll(cx, |cx, i| {
257 ready!(i.poll_ready(cx));
258 Poll::Ready(i.check_write().map(|_| ()))
259 }));
260 self.flushing = false;
261 match result {
262 Some(result) => Poll::Ready(result),
263 None => Poll::Ready(Ok(())),
264 }
265 }
266 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
267 Poll::Ready(Ok(()))
268 }
269}
270
271/// State necessary for effectively transforming `Arc<Mutex<dyn
272/// {Input,Output}Stream>>` into `Async{Read,Write}`.
273///
274/// This is a beast and inefficient. It should get the job done in theory but
275/// one must truly ask oneself at some point "but at what cost".
276///
277/// More seriously, it's unclear if this is the best way to transform a single
278/// `AsyncRead` into a "multiple `AsyncRead`". This certainly is an attempt and
279/// the hope is that everything here is private enough that we can refactor as
280/// necessary in the future without causing much churn.
281enum StdioHandle<S> {
282 Ready(Arc<Mutex<S>>),
283 Locking(Box<dyn Future<Output = OwnedMutexGuard<S>> + Send + Sync>),
284 Locked(OwnedMutexGuard<S>),
285 Closed,
286}
287
288impl<S> StdioHandle<S>
289where
290 S: Send + Sync + 'static,
291{
292 fn poll<T>(
293 mut self: Pin<&mut Self>,
294 cx: &mut Context<'_>,
295 op: impl FnOnce(&mut Context<'_>, &mut S) -> Poll<p2::StreamResult<T>>,
296 ) -> Poll<Option<io::Result<T>>> {
297 // If we don't currently have the lock on this handle, initiate the
298 // lock acquisition.
299 if let StdioHandle::Ready(lock) = &*self {
300 self.set(StdioHandle::Locking(Box::new(lock.clone().lock_owned())));
301 }
302
303 // If we're in the process of locking this handle, wait for that to
304 // finish.
305 if let Some(lock) = self.as_mut().as_locking() {
306 let guard = ready!(lock.poll(cx));
307 self.set(StdioHandle::Locked(guard));
308 }
309
310 let mut guard = match self.as_mut().take_guard() {
311 Some(guard) => guard,
312 // If the guard can't be acquired that means that this stream is
313 // closed, so return that we're ready without filling in data.
314 None => return Poll::Ready(None),
315 };
316
317 // Perform the I/O and delegate on the result.
318 let result = match op(cx, &mut guard) {
319 Poll::Ready(result) => result,
320
321 // If the stream isn't ready yet then restore our "locked" state
322 // since we haven't finished, then return pending.
323 Poll::Pending => {
324 self.set(StdioHandle::Locked(guard));
325 return Poll::Pending;
326 }
327 };
328 match result {
329 // The I/O succeeded so relinquish the lock on this stream by
330 // transitioning back to the "Ready" state.
331 Ok(result) => {
332 self.set(StdioHandle::Ready(OwnedMutexGuard::mutex(&guard).clone()));
333 Poll::Ready(Some(Ok(result)))
334 }
335
336 // The stream is closed, and `take_guard` above already set the
337 // closed state, so return nothing indicating the closure.
338 Err(p2::StreamError::Closed) => Poll::Ready(None),
339
340 // The stream failed so propagate the error. Errors should only
341 // come from the underlying I/O object and thus should cast
342 // successfully. Additionally `take_guard` replaced our state
343 // with "closed" above which is the desired state at this point.
344 Err(p2::StreamError::LastOperationFailed(e)) => {
345 Poll::Ready(Some(Err(e.downcast().unwrap())))
346 }
347
348 // Shouldn't be possible to produce a trap here.
349 Err(p2::StreamError::Trap(_)) => unreachable!(),
350 }
351 }
352
353 fn as_locking(
354 self: Pin<&mut Self>,
355 ) -> Option<Pin<&mut dyn Future<Output = OwnedMutexGuard<S>>>> {
356 // SAFETY: this is a pin-projection from `self` into the `Locking`
357 // field.
358 unsafe {
359 match self.get_unchecked_mut() {
360 StdioHandle::Locking(future) => Some(Pin::new_unchecked(&mut **future)),
361 _ => None,
362 }
363 }
364 }
365
366 fn take_guard(self: Pin<&mut Self>) -> Option<OwnedMutexGuard<S>> {
367 if !matches!(*self, StdioHandle::Locked(_)) {
368 return None;
369 }
370 // SAFETY: the `Locked` arm is safe to move as it's an invariant of this
371 // type that it's not pinned.
372 unsafe {
373 match mem::replace(self.get_unchecked_mut(), StdioHandle::Closed) {
374 StdioHandle::Locked(guard) => Some(guard),
375 _ => unreachable!(),
376 }
377 }
378 }
379}