From 585f9fb3f841b5e20157f7714d9e2647deca84f1 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 1 Jun 2023 14:51:00 -0700 Subject: [PATCH 001/118] preview2: make everything but streams/io and poll/poll synchronous --- crates/wasi/src/preview2/preview1/mod.rs | 327 ++++++++---------- crates/wasi/src/preview2/preview2/clocks.rs | 23 +- crates/wasi/src/preview2/preview2/env.rs | 19 +- crates/wasi/src/preview2/preview2/exit.rs | 3 +- .../wasi/src/preview2/preview2/filesystem.rs | 80 ++--- crates/wasi/src/preview2/preview2/random.rs | 13 +- crates/wasi/src/preview2/wasi/mod.rs | 76 ++-- 7 files changed, 249 insertions(+), 292 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index af46bf27f496..cba244044410 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -4,7 +4,7 @@ use crate::preview2::wasi::cli_base::{preopens, stderr, stdin, stdout}; use crate::preview2::wasi::clocks::monotonic_clock; use crate::preview2::wasi::clocks::wall_clock; use crate::preview2::wasi::filesystem::filesystem; -use crate::preview2::wasi::io::streams; +use crate::preview2::wasi::sync_io::io::streams; use crate::preview2::{wasi, TableError, WasiView}; use anyhow::{anyhow, bail, Context}; use std::borrow::Borrow; @@ -71,27 +71,23 @@ impl DerefMut for Descriptors { impl Descriptors { /// Initializes [Self] using `preopens` - async fn new( + fn new( preopens: &mut (impl preopens::Host + stdin::Host + stdout::Host + stderr::Host + ?Sized), ) -> Result { let stdin = preopens .get_stdin() - .await .context("failed to call `get-stdin`") .map_err(types::Error::trap)?; let stdout = preopens .get_stdout() - .await .context("failed to call `get-stdout`") .map_err(types::Error::trap)?; let stderr = preopens .get_stderr() - .await .context("failed to call `get-stderr`") .map_err(types::Error::trap)?; let directories = preopens .get_directories() - .await .context("failed to call `get-directories`") .map_err(types::Error::trap)?; @@ -272,17 +268,16 @@ impl Transaction<'_, T> { } } -#[wiggle::async_trait] trait WasiPreview1ViewExt: WasiPreview1View + preopens::Host + stdin::Host + stdout::Host + stderr::Host { /// Lazily initializes [`WasiPreview1Adapter`] returned by [`WasiPreview1View::adapter_mut`] /// and returns [`Transaction`] on success - async fn transact(&mut self) -> Result, types::Error> { + fn transact(&mut self) -> Result, types::Error> { let descriptors = if let Some(descriptors) = self.adapter_mut().descriptors.take() { descriptors } else { - Descriptors::new(self).await? + Descriptors::new(self)? } .into(); Ok(Transaction { @@ -293,8 +288,8 @@ trait WasiPreview1ViewExt: /// Lazily initializes [`WasiPreview1Adapter`] returned by [`WasiPreview1View::adapter_mut`] /// and returns [`filesystem::Descriptor`] corresponding to `fd` - async fn get_fd(&mut self, fd: types::Fd) -> Result { - let mut st = self.transact().await?; + fn get_fd(&mut self, fd: types::Fd) -> Result { + let mut st = self.transact()?; let fd = st.get_fd(fd)?; Ok(fd) } @@ -302,8 +297,8 @@ trait WasiPreview1ViewExt: /// Lazily initializes [`WasiPreview1Adapter`] returned by [`WasiPreview1View::adapter_mut`] /// and returns [`filesystem::Descriptor`] corresponding to `fd` /// if it describes a [`Descriptor::File`] of [`crate::preview2::filesystem::File`] type - async fn get_file_fd(&mut self, fd: types::Fd) -> Result { - let mut st = self.transact().await?; + fn get_file_fd(&mut self, fd: types::Fd) -> Result { + let mut st = self.transact()?; let fd = st.get_file_fd(fd)?; Ok(fd) } @@ -312,8 +307,8 @@ trait WasiPreview1ViewExt: /// and returns [`filesystem::Descriptor`] corresponding to `fd` /// if it describes a [`Descriptor::File`] or [`Descriptor::PreopenDirectory`] /// of [`crate::preview2::filesystem::Dir`] type - async fn get_dir_fd(&mut self, fd: types::Fd) -> Result { - let mut st = self.transact().await?; + fn get_dir_fd(&mut self, fd: types::Fd) -> Result { + let mut st = self.transact()?; let fd = st.get_dir_fd(fd)?; Ok(fd) } @@ -327,9 +322,9 @@ pub fn add_to_linker< + wasi::cli_base::exit::Host + wasi::cli_base::preopens::Host + wasi::filesystem::filesystem::Host - + wasi::poll::poll::Host + + wasi::sync_io::poll::poll::Host + wasi::random::random::Host - + wasi::io::streams::Host + + wasi::sync_io::io::streams::Host + wasi::clocks::monotonic_clock::Host + wasi::clocks::wall_clock::Host, >( @@ -345,7 +340,6 @@ pub fn add_to_linker< wiggle::from_witx!({ witx: ["$CARGO_MANIFEST_DIR/witx/wasi_snapshot_preview1.witx"], errors: { errno => trappable Error }, - async: *, }); impl wiggle::GuestErrorType for types::Errno { @@ -620,28 +614,26 @@ fn first_non_empty_iovec<'a>( // Implement the WasiSnapshotPreview1 trait using only the traits that are // required for T, i.e., in terms of the preview 2 wit interface, and state // stored in the WasiPreview1Adapter struct. -#[wiggle::async_trait] impl< T: WasiPreview1View + wasi::cli_base::environment::Host + wasi::cli_base::exit::Host + wasi::cli_base::preopens::Host + wasi::filesystem::filesystem::Host - + wasi::poll::poll::Host + + wasi::sync_io::poll::poll::Host + wasi::random::random::Host - + wasi::io::streams::Host + + wasi::sync_io::io::streams::Host + wasi::clocks::monotonic_clock::Host + wasi::clocks::wall_clock::Host, > wasi_snapshot_preview1::WasiSnapshotPreview1 for T { #[instrument(skip(self))] - async fn args_get<'b>( + fn args_get<'b>( &mut self, argv: &GuestPtr<'b, GuestPtr<'b, u8>>, argv_buf: &GuestPtr<'b, u8>, ) -> Result<(), types::Error> { self.get_arguments() - .await .context("failed to call `get-arguments`") .map_err(types::Error::trap)? .into_iter() @@ -663,10 +655,9 @@ impl< } #[instrument(skip(self))] - async fn args_sizes_get(&mut self) -> Result<(types::Size, types::Size), types::Error> { + fn args_sizes_get(&mut self) -> Result<(types::Size, types::Size), types::Error> { let args = self .get_arguments() - .await .context("failed to call `get-arguments`") .map_err(types::Error::trap)?; let num = args.len().try_into().map_err(|_| types::Errno::Overflow)?; @@ -680,13 +671,12 @@ impl< } #[instrument(skip(self))] - async fn environ_get<'b>( + fn environ_get<'b>( &mut self, environ: &GuestPtr<'b, GuestPtr<'b, u8>>, environ_buf: &GuestPtr<'b, u8>, ) -> Result<(), types::Error> { self.get_environment() - .await .context("failed to call `get-environment`") .map_err(types::Error::trap)? .into_iter() @@ -712,10 +702,9 @@ impl< } #[instrument(skip(self))] - async fn environ_sizes_get(&mut self) -> Result<(types::Size, types::Size), types::Error> { + fn environ_sizes_get(&mut self) -> Result<(types::Size, types::Size), types::Error> { let environ = self .get_environment() - .await .context("failed to call `get-environment`") .map_err(types::Error::trap)?; let num = environ @@ -732,18 +721,13 @@ impl< } #[instrument(skip(self))] - async fn clock_res_get( - &mut self, - id: types::Clockid, - ) -> Result { + fn clock_res_get(&mut self, id: types::Clockid) -> Result { let res = match id { types::Clockid::Realtime => wall_clock::Host::resolution(self) - .await .context("failed to call `wall_clock::resolution`") .map_err(types::Error::trap)? .try_into()?, types::Clockid::Monotonic => monotonic_clock::Host::resolution(self) - .await .context("failed to call `monotonic_clock::resolution`") .map_err(types::Error::trap)?, types::Clockid::ProcessCputimeId | types::Clockid::ThreadCputimeId => { @@ -754,19 +738,17 @@ impl< } #[instrument(skip(self))] - async fn clock_time_get( + fn clock_time_get( &mut self, id: types::Clockid, _precision: types::Timestamp, ) -> Result { let now = match id { types::Clockid::Realtime => wall_clock::Host::now(self) - .await .context("failed to call `wall_clock::now`") .map_err(types::Error::trap)? .try_into()?, types::Clockid::Monotonic => monotonic_clock::Host::now(self) - .await .context("failed to call `monotonic_clock::now`") .map_err(types::Error::trap)?, types::Clockid::ProcessCputimeId | types::Clockid::ThreadCputimeId => { @@ -777,60 +759,54 @@ impl< } #[instrument(skip(self))] - async fn fd_advise( + fn fd_advise( &mut self, fd: types::Fd, offset: types::Filesize, len: types::Filesize, advice: types::Advice, ) -> Result<(), types::Error> { - let fd = self.get_file_fd(fd).await?; - self.advise(fd, offset, len, advice.into()) - .await - .map_err(|e| { - e.try_into() - .context("failed to call `advise`") - .unwrap_or_else(types::Error::trap) - }) + let fd = self.get_file_fd(fd)?; + self.advise(fd, offset, len, advice.into()).map_err(|e| { + e.try_into() + .context("failed to call `advise`") + .unwrap_or_else(types::Error::trap) + }) } /// Force the allocation of space in a file. /// NOTE: This is similar to `posix_fallocate` in POSIX. #[instrument(skip(self))] - async fn fd_allocate( + fn fd_allocate( &mut self, fd: types::Fd, _offset: types::Filesize, _len: types::Filesize, ) -> Result<(), types::Error> { - self.get_file_fd(fd).await?; + self.get_file_fd(fd)?; Err(types::Errno::Notsup.into()) } /// Close a file descriptor. /// NOTE: This is similar to `close` in POSIX. #[instrument(skip(self))] - async fn fd_close(&mut self, fd: types::Fd) -> Result<(), types::Error> { + fn fd_close(&mut self, fd: types::Fd) -> Result<(), types::Error> { let desc = self - .transact() - .await? + .transact()? .descriptors .get_mut() .remove(fd) .ok_or(types::Errno::Badf)? .clone(); match desc { - Descriptor::Stdin(stream) => self - .drop_input_stream(stream) - .await + Descriptor::Stdin(stream) => streams::Host::drop_input_stream(self, stream) .context("failed to call `drop-input-stream`"), - Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => self - .drop_output_stream(stream) - .await - .context("failed to call `drop-output-stream`"), + Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => { + streams::Host::drop_output_stream(self, stream) + .context("failed to call `drop-output-stream`") + } Descriptor::File(File { fd, .. }) | Descriptor::PreopenDirectory((fd, _)) => self .drop_descriptor(fd) - .await .context("failed to call `drop-descriptor`"), } .map_err(types::Error::trap) @@ -839,9 +815,9 @@ impl< /// Synchronize the data of a file to disk. /// NOTE: This is similar to `fdatasync` in POSIX. #[instrument(skip(self))] - async fn fd_datasync(&mut self, fd: types::Fd) -> Result<(), types::Error> { - let fd = self.get_file_fd(fd).await?; - self.sync_data(fd).await.map_err(|e| { + fn fd_datasync(&mut self, fd: types::Fd) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.sync_data(fd).map_err(|e| { e.try_into() .context("failed to call `sync-data`") .unwrap_or_else(types::Error::trap) @@ -851,8 +827,8 @@ impl< /// Get the attributes of a file descriptor. /// NOTE: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. #[instrument(skip(self))] - async fn fd_fdstat_get(&mut self, fd: types::Fd) -> Result { - let (fd, blocking, append) = match self.transact().await?.get_descriptor(fd)? { + fn fd_fdstat_get(&mut self, fd: types::Fd) -> Result { + let (fd, blocking, append) = match self.transact()?.get_descriptor(fd)? { Descriptor::Stdin(..) => { let fs_rights_base = types::Rights::FD_READ; return Ok(types::Fdstat { @@ -882,14 +858,13 @@ impl< // TODO: use `try_join!` to poll both futures async, unfortunately that is not currently // possible, because `bindgen` generates methods with `&mut self` receivers. - let flags = self.get_flags(fd).await.map_err(|e| { + let flags = self.get_flags(fd).map_err(|e| { e.try_into() .context("failed to call `get-flags`") .unwrap_or_else(types::Error::trap) })?; let fs_filetype = self .get_type(fd) - .await .map_err(|e| { e.try_into() .context("failed to call `get-type`") @@ -931,12 +906,12 @@ impl< /// Adjust the flags associated with a file descriptor. /// NOTE: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. #[instrument(skip(self))] - async fn fd_fdstat_set_flags( + fn fd_fdstat_set_flags( &mut self, fd: types::Fd, flags: types::Fdflags, ) -> Result<(), types::Error> { - let mut st = self.transact().await?; + let mut st = self.transact()?; let File { append, blocking, .. } = st.get_file_mut(fd)?; @@ -955,20 +930,20 @@ impl< /// Does not do anything if `fd` corresponds to a valid descriptor and returns `[types::Errno::Badf]` error otherwise. #[instrument(skip(self))] - async fn fd_fdstat_set_rights( + fn fd_fdstat_set_rights( &mut self, fd: types::Fd, _fs_rights_base: types::Rights, _fs_rights_inheriting: types::Rights, ) -> Result<(), types::Error> { - self.get_fd(fd).await?; + self.get_fd(fd)?; Ok(()) } /// Return the attributes of an open file. #[instrument(skip(self))] - async fn fd_filestat_get(&mut self, fd: types::Fd) -> Result { - let desc = self.transact().await?.get_descriptor(fd)?.clone(); + fn fd_filestat_get(&mut self, fd: types::Fd) -> Result { + let desc = self.transact()?.get_descriptor(fd)?.clone(); match desc { Descriptor::Stdin(..) | Descriptor::Stdout(..) | Descriptor::Stderr(..) => { Ok(types::Filestat { @@ -992,7 +967,7 @@ impl< data_access_timestamp, data_modification_timestamp, status_change_timestamp, - } = self.stat(fd).await.map_err(|e| { + } = self.stat(fd).map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -1018,13 +993,13 @@ impl< /// Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. /// NOTE: This is similar to `ftruncate` in POSIX. #[instrument(skip(self))] - async fn fd_filestat_set_size( + fn fd_filestat_set_size( &mut self, fd: types::Fd, size: types::Filesize, ) -> Result<(), types::Error> { - let fd = self.get_file_fd(fd).await?; - self.set_size(fd, size).await.map_err(|e| { + let fd = self.get_file_fd(fd)?; + self.set_size(fd, size).map_err(|e| { e.try_into() .context("failed to call `set-size`") .unwrap_or_else(types::Error::trap) @@ -1034,7 +1009,7 @@ impl< /// Adjust the timestamps of an open file or directory. /// NOTE: This is similar to `futimens` in POSIX. #[instrument(skip(self))] - async fn fd_filestat_set_times( + fn fd_filestat_set_times( &mut self, fd: types::Fd, atim: types::Timestamp, @@ -1052,8 +1027,8 @@ impl< fst_flags.contains(types::Fstflags::MTIM_NOW), )?; - let fd = self.get_fd(fd).await?; - self.set_times(fd, atim, mtim).await.map_err(|e| { + let fd = self.get_fd(fd)?; + self.set_times(fd, atim, mtim).map_err(|e| { e.try_into() .context("failed to call `set-times`") .unwrap_or_else(types::Error::trap) @@ -1063,12 +1038,12 @@ impl< /// Read from a file descriptor. /// NOTE: This is similar to `readv` in POSIX. #[instrument(skip(self))] - async fn fd_read<'a>( + fn fd_read<'a>( &mut self, fd: types::Fd, iovs: &types::IovecArray<'a>, ) -> Result { - let desc = self.transact().await?.get_descriptor(fd)?.clone(); + let desc = self.transact()?.get_descriptor(fd)?.clone(); let (mut buf, read, end) = match desc { Descriptor::File(File { fd, @@ -1081,18 +1056,17 @@ impl< }; let pos = position.load(Ordering::Relaxed); - let stream = self.read_via_stream(fd, pos).await.map_err(|e| { + let stream = self.read_via_stream(fd, pos).map_err(|e| { e.try_into() .context("failed to call `read-via-stream`") .unwrap_or_else(types::Error::trap) })?; let max = buf.len().try_into().unwrap_or(u64::MAX); let (read, end) = if blocking { - self.blocking_read(stream, max) + streams::Host::blocking_read(self, stream, max) } else { streams::Host::read(self, stream, max) } - .await .map_err(|_| types::Errno::Io)?; let n = read.len().try_into().or(Err(types::Errno::Overflow))?; @@ -1107,7 +1081,6 @@ impl< }; let (read, end) = streams::Host::read(self, stream, buf.len().try_into().unwrap_or(u64::MAX)) - .await .map_err(|_| types::Errno::Io)?; (buf, read, end) } @@ -1128,31 +1101,30 @@ impl< /// Read from a file descriptor, without using and updating the file descriptor's offset. /// NOTE: This is similar to `preadv` in POSIX. #[instrument(skip(self))] - async fn fd_pread<'a>( + fn fd_pread<'a>( &mut self, fd: types::Fd, iovs: &types::IovecArray<'a>, offset: types::Filesize, ) -> Result { - let desc = self.transact().await?.get_descriptor(fd)?.clone(); + let desc = self.transact()?.get_descriptor(fd)?.clone(); let (mut buf, read, end) = match desc { Descriptor::File(File { fd, blocking, .. }) if self.table().is_file(fd) => { let Some(buf) = first_non_empty_iovec(iovs)? else { return Ok(0) }; - let stream = self.read_via_stream(fd, offset).await.map_err(|e| { + let stream = self.read_via_stream(fd, offset).map_err(|e| { e.try_into() .context("failed to call `read-via-stream`") .unwrap_or_else(types::Error::trap) })?; let max = buf.len().try_into().unwrap_or(u64::MAX); let (read, end) = if blocking { - self.blocking_read(stream, max) + streams::Host::blocking_read(self, stream, max) } else { streams::Host::read(self, stream, max) } - .await .map_err(|_| types::Errno::Io)?; (buf, read, end) @@ -1178,12 +1150,12 @@ impl< /// Write to a file descriptor. /// NOTE: This is similar to `writev` in POSIX. #[instrument(skip(self))] - async fn fd_write<'a>( + fn fd_write<'a>( &mut self, fd: types::Fd, ciovs: &types::CiovecArray<'a>, ) -> Result { - let desc = self.transact().await?.get_descriptor(fd)?.clone(); + let desc = self.transact()?.get_descriptor(fd)?.clone(); let n = match desc { Descriptor::File(File { fd, @@ -1195,7 +1167,7 @@ impl< return Ok(0) }; let (stream, pos) = if append { - let stream = self.append_via_stream(fd).await.map_err(|e| { + let stream = self.append_via_stream(fd).map_err(|e| { e.try_into() .context("failed to call `append-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1203,7 +1175,7 @@ impl< (stream, 0) } else { let position = position.load(Ordering::Relaxed); - let stream = self.write_via_stream(fd, position).await.map_err(|e| { + let stream = self.write_via_stream(fd, position).map_err(|e| { e.try_into() .context("failed to call `write-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1211,11 +1183,10 @@ impl< (stream, position) }; let n = if blocking { - self.blocking_write(stream, buf) + streams::Host::blocking_write(self, stream, buf) } else { streams::Host::write(self, stream, buf) } - .await .map_err(|_| types::Errno::Io)?; if !append { let pos = pos.checked_add(n).ok_or(types::Errno::Overflow)?; @@ -1227,9 +1198,7 @@ impl< let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - streams::Host::write(self, stream, buf) - .await - .map_err(|_| types::Errno::Io)? + streams::Host::write(self, stream, buf).map_err(|_| types::Errno::Io)? } _ => return Err(types::Errno::Badf.into()), } @@ -1241,29 +1210,28 @@ impl< /// Write to a file descriptor, without using and updating the file descriptor's offset. /// NOTE: This is similar to `pwritev` in POSIX. #[instrument(skip(self))] - async fn fd_pwrite<'a>( + fn fd_pwrite<'a>( &mut self, fd: types::Fd, ciovs: &types::CiovecArray<'a>, offset: types::Filesize, ) -> Result { - let desc = self.transact().await?.get_descriptor(fd)?.clone(); + let desc = self.transact()?.get_descriptor(fd)?.clone(); let n = match desc { Descriptor::File(File { fd, blocking, .. }) if self.table().is_file(fd) => { let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - let stream = self.write_via_stream(fd, offset).await.map_err(|e| { + let stream = self.write_via_stream(fd, offset).map_err(|e| { e.try_into() .context("failed to call `write-via-stream`") .unwrap_or_else(types::Error::trap) })?; if blocking { - self.blocking_write(stream, buf) + streams::Host::blocking_write(self, stream, buf) } else { streams::Host::write(self, stream, buf) } - .await .map_err(|_| types::Errno::Io)? } Descriptor::Stdout(..) | Descriptor::Stderr(..) => { @@ -1279,8 +1247,8 @@ impl< /// Return a description of the given preopened file descriptor. #[instrument(skip(self))] - async fn fd_prestat_get(&mut self, fd: types::Fd) -> Result { - if let Descriptor::PreopenDirectory((_, p)) = self.transact().await?.get_descriptor(fd)? { + fn fd_prestat_get(&mut self, fd: types::Fd) -> Result { + if let Descriptor::PreopenDirectory((_, p)) = self.transact()?.get_descriptor(fd)? { let pr_name_len = p.len().try_into().or(Err(types::Errno::Overflow))?; return Ok(types::Prestat::Dir(types::PrestatDir { pr_name_len })); } @@ -1289,14 +1257,14 @@ impl< /// Return a description of the given preopened file descriptor. #[instrument(skip(self))] - async fn fd_prestat_dir_name<'a>( + fn fd_prestat_dir_name<'a>( &mut self, fd: types::Fd, path: &GuestPtr<'a, u8>, path_max_len: types::Size, ) -> Result<(), types::Error> { let path_max_len = path_max_len.try_into().or(Err(types::Errno::Overflow))?; - if let Descriptor::PreopenDirectory((_, p)) = self.transact().await?.get_descriptor(fd)? { + if let Descriptor::PreopenDirectory((_, p)) = self.transact()?.get_descriptor(fd)? { if p.len() > path_max_len { return Err(types::Errno::Nametoolong.into()); } @@ -1308,8 +1276,8 @@ impl< /// Atomically replace a file descriptor by renumbering another file descriptor. #[instrument(skip(self))] - async fn fd_renumber(&mut self, from: types::Fd, to: types::Fd) -> Result<(), types::Error> { - let mut st = self.transact().await?; + fn fd_renumber(&mut self, from: types::Fd, to: types::Fd) -> Result<(), types::Error> { + let mut st = self.transact()?; let descriptors = st.descriptors.get_mut(); let desc = descriptors.remove(from).ok_or(types::Errno::Badf)?; descriptors.insert(to.into(), desc); @@ -1319,14 +1287,14 @@ impl< /// Move the offset of a file descriptor. /// NOTE: This is similar to `lseek` in POSIX. #[instrument(skip(self))] - async fn fd_seek( + fn fd_seek( &mut self, fd: types::Fd, offset: types::Filedelta, whence: types::Whence, ) -> Result { let (fd, position) = { - let mut st = self.transact().await?; + let mut st = self.transact()?; let File { fd, position, .. } = st.get_seekable(fd)?; (*fd, Arc::clone(&position)) }; @@ -1337,7 +1305,7 @@ impl< .checked_add_signed(offset) .ok_or(types::Errno::Inval)?, types::Whence::End => { - let filesystem::DescriptorStat { size, .. } = self.stat(fd).await.map_err(|e| { + let filesystem::DescriptorStat { size, .. } = self.stat(fd).map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -1353,9 +1321,9 @@ impl< /// Synchronize the data and metadata of a file to disk. /// NOTE: This is similar to `fsync` in POSIX. #[instrument(skip(self))] - async fn fd_sync(&mut self, fd: types::Fd) -> Result<(), types::Error> { - let fd = self.get_file_fd(fd).await?; - self.sync(fd).await.map_err(|e| { + fn fd_sync(&mut self, fd: types::Fd) -> Result<(), types::Error> { + let fd = self.get_file_fd(fd)?; + self.sync(fd).map_err(|e| { e.try_into() .context("failed to call `sync`") .unwrap_or_else(types::Error::trap) @@ -1365,32 +1333,31 @@ impl< /// Return the current offset of a file descriptor. /// NOTE: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. #[instrument(skip(self))] - async fn fd_tell(&mut self, fd: types::Fd) -> Result { + fn fd_tell(&mut self, fd: types::Fd) -> Result { let pos = self - .transact() - .await? + .transact()? .get_seekable(fd) .map(|File { position, .. }| position.load(Ordering::Relaxed))?; Ok(pos) } #[instrument(skip(self))] - async fn fd_readdir<'a>( + fn fd_readdir<'a>( &mut self, fd: types::Fd, buf: &GuestPtr<'a, u8>, buf_len: types::Size, cookie: types::Dircookie, ) -> Result { - let fd = self.get_dir_fd(fd).await?; - let stream = self.read_directory(fd).await.map_err(|e| { + let fd = self.get_dir_fd(fd)?; + let stream = self.read_directory(fd).map_err(|e| { e.try_into() .context("failed to call `read-directory`") .unwrap_or_else(types::Error::trap) })?; let filesystem::DescriptorStat { inode: fd_inode, .. - } = self.stat(fd).await.map_err(|e| { + } = self.stat(fd).map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -1485,14 +1452,14 @@ impl< } #[instrument(skip(self))] - async fn path_create_directory<'a>( + fn path_create_directory<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - self.create_directory_at(dirfd, path).await.map_err(|e| { + self.create_directory_at(dirfd, path).map_err(|e| { e.try_into() .context("failed to call `create-directory-at`") .unwrap_or_else(types::Error::trap) @@ -1502,13 +1469,13 @@ impl< /// Return the attributes of a file or directory. /// NOTE: This is similar to `stat` in POSIX. #[instrument(skip(self))] - async fn path_filestat_get<'a>( + fn path_filestat_get<'a>( &mut self, dirfd: types::Fd, flags: types::Lookupflags, path: &GuestPtr<'a, str>, ) -> Result { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; let filesystem::DescriptorStat { device: dev, @@ -1519,7 +1486,7 @@ impl< data_access_timestamp, data_modification_timestamp, status_change_timestamp, - } = self.stat_at(dirfd, flags.into(), path).await.map_err(|e| { + } = self.stat_at(dirfd, flags.into(), path).map_err(|e| { e.try_into() .context("failed to call `stat-at`") .unwrap_or_else(types::Error::trap) @@ -1543,7 +1510,7 @@ impl< /// Adjust the timestamps of a file or directory. /// NOTE: This is similar to `utimensat` in POSIX. #[instrument(skip(self))] - async fn path_filestat_set_times<'a>( + fn path_filestat_set_times<'a>( &mut self, dirfd: types::Fd, flags: types::Lookupflags, @@ -1563,10 +1530,9 @@ impl< fst_flags.contains(types::Fstflags::MTIM_NOW), )?; - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; self.set_times_at(dirfd, flags.into(), path, atim, mtim) - .await .map_err(|e| { e.try_into() .context("failed to call `set-times-at`") @@ -1577,7 +1543,7 @@ impl< /// Create a hard link. /// NOTE: This is similar to `linkat` in POSIX. #[instrument(skip(self))] - async fn path_link<'a>( + fn path_link<'a>( &mut self, src_fd: types::Fd, src_flags: types::Lookupflags, @@ -1585,12 +1551,11 @@ impl< target_fd: types::Fd, target_path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let src_fd = self.get_dir_fd(src_fd).await?; - let target_fd = self.get_dir_fd(target_fd).await?; + let src_fd = self.get_dir_fd(src_fd)?; + let target_fd = self.get_dir_fd(target_fd)?; let src_path = read_string(src_path)?; let target_path = read_string(target_path)?; self.link_at(src_fd, src_flags.into(), src_path, target_fd, target_path) - .await .map_err(|e| { e.try_into() .context("failed to call `link-at`") @@ -1601,7 +1566,7 @@ impl< /// Open a file or directory. /// NOTE: This is similar to `openat` in POSIX. #[instrument(skip(self))] - async fn path_open<'a>( + fn path_open<'a>( &mut self, dirfd: types::Fd, dirflags: types::Lookupflags, @@ -1630,7 +1595,7 @@ impl< flags |= filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC; } - let desc = self.transact().await?.get_descriptor(dirfd)?.clone(); + let desc = self.transact()?.get_descriptor(dirfd)?.clone(); let dirfd = match desc { Descriptor::PreopenDirectory((fd, _)) => fd, Descriptor::File(File { fd, .. }) if self.table().is_dir(fd) => fd, @@ -1649,39 +1614,33 @@ impl< flags, filesystem::Modes::READABLE | filesystem::Modes::WRITABLE, ) - .await .map_err(|e| { e.try_into() .context("failed to call `open-at`") .unwrap_or_else(types::Error::trap) })?; - let fd = self - .transact() - .await? - .descriptors - .get_mut() - .push_file(File { - fd, - position: Default::default(), - append: fdflags.contains(types::Fdflags::APPEND), - blocking: !fdflags.contains(types::Fdflags::NONBLOCK), - })?; + let fd = self.transact()?.descriptors.get_mut().push_file(File { + fd, + position: Default::default(), + append: fdflags.contains(types::Fdflags::APPEND), + blocking: !fdflags.contains(types::Fdflags::NONBLOCK), + })?; Ok(fd.into()) } /// Read the contents of a symbolic link. /// NOTE: This is similar to `readlinkat` in POSIX. #[instrument(skip(self))] - async fn path_readlink<'a>( + fn path_readlink<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, buf: &GuestPtr<'a, u8>, buf_len: types::Size, ) -> Result { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - let mut path = self.readlink_at(dirfd, path).await.map_err(|e| { + let mut path = self.readlink_at(dirfd, path).map_err(|e| { e.try_into() .context("failed to call `readlink-at`") .unwrap_or_else(types::Error::trap) @@ -1696,14 +1655,14 @@ impl< } #[instrument(skip(self))] - async fn path_remove_directory<'a>( + fn path_remove_directory<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - self.remove_directory_at(dirfd, path).await.map_err(|e| { + self.remove_directory_at(dirfd, path).map_err(|e| { e.try_into() .context("failed to call `remove-directory-at`") .unwrap_or_else(types::Error::trap) @@ -1713,19 +1672,18 @@ impl< /// Rename a file or directory. /// NOTE: This is similar to `renameat` in POSIX. #[instrument(skip(self))] - async fn path_rename<'a>( + fn path_rename<'a>( &mut self, src_fd: types::Fd, src_path: &GuestPtr<'a, str>, dest_fd: types::Fd, dest_path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let src_fd = self.get_dir_fd(src_fd).await?; - let dest_fd = self.get_dir_fd(dest_fd).await?; + let src_fd = self.get_dir_fd(src_fd)?; + let dest_fd = self.get_dir_fd(dest_fd)?; let src_path = read_string(src_path)?; let dest_path = read_string(dest_path)?; self.rename_at(src_fd, src_path, dest_fd, dest_path) - .await .map_err(|e| { e.try_into() .context("failed to call `rename-at`") @@ -1734,33 +1692,31 @@ impl< } #[instrument(skip(self))] - async fn path_symlink<'a>( + fn path_symlink<'a>( &mut self, src_path: &GuestPtr<'a, str>, dirfd: types::Fd, dest_path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let src_path = read_string(src_path)?; let dest_path = read_string(dest_path)?; - self.symlink_at(dirfd, src_path, dest_path) - .await - .map_err(|e| { - e.try_into() - .context("failed to call `symlink-at`") - .unwrap_or_else(types::Error::trap) - }) + self.symlink_at(dirfd, src_path, dest_path).map_err(|e| { + e.try_into() + .context("failed to call `symlink-at`") + .unwrap_or_else(types::Error::trap) + }) } #[instrument(skip(self))] - async fn path_unlink_file<'a>( + fn path_unlink_file<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { - let dirfd = self.get_dir_fd(dirfd).await?; + let dirfd = self.get_dir_fd(dirfd)?; let path = path.as_cow().map_err(|_| types::Errno::Inval)?.to_string(); - self.unlink_file_at(dirfd, path).await.map_err(|e| { + self.unlink_file_at(dirfd, path).map_err(|e| { e.try_into() .context("failed to call `unlink-file-at`") .unwrap_or_else(types::Error::trap) @@ -1769,7 +1725,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - async fn poll_oneoff<'a>( + fn poll_oneoff<'a>( &mut self, subs: &GuestPtr<'a, types::Subscription>, events: &GuestPtr<'a, types::Event>, @@ -1779,37 +1735,36 @@ impl< } #[instrument(skip(self))] - async fn proc_exit(&mut self, status: types::Exitcode) -> anyhow::Error { + fn proc_exit(&mut self, status: types::Exitcode) -> anyhow::Error { let status = match status { 0 => Ok(()), _ => Err(()), }; - match self.exit(status).await { + match self.exit(status) { Err(e) => e, Ok(()) => anyhow!("`exit` did not return an error"), } } #[instrument(skip(self))] - async fn proc_raise(&mut self, _sig: types::Signal) -> Result<(), types::Error> { + fn proc_raise(&mut self, _sig: types::Signal) -> Result<(), types::Error> { Err(types::Errno::Notsup.into()) } #[instrument(skip(self))] - async fn sched_yield(&mut self) -> Result<(), types::Error> { + fn sched_yield(&mut self) -> Result<(), types::Error> { // TODO: This is not yet covered in Preview2. Ok(()) } #[instrument(skip(self))] - async fn random_get<'a>( + fn random_get<'a>( &mut self, buf: &GuestPtr<'a, u8>, buf_len: types::Size, ) -> Result<(), types::Error> { let rand = self .get_random_bytes(buf_len.into()) - .await .context("failed to call `get-random-bytes`") .map_err(types::Error::trap)?; write_bytes(buf, rand)?; @@ -1818,7 +1773,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - async fn sock_accept( + fn sock_accept( &mut self, fd: types::Fd, flags: types::Fdflags, @@ -1828,7 +1783,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - async fn sock_recv<'a>( + fn sock_recv<'a>( &mut self, fd: types::Fd, ri_data: &types::IovecArray<'a>, @@ -1839,7 +1794,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - async fn sock_send<'a>( + fn sock_send<'a>( &mut self, fd: types::Fd, si_data: &types::CiovecArray<'a>, @@ -1850,11 +1805,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - async fn sock_shutdown( - &mut self, - fd: types::Fd, - how: types::Sdflags, - ) -> Result<(), types::Error> { + fn sock_shutdown(&mut self, fd: types::Fd, how: types::Sdflags) -> Result<(), types::Error> { todo!() } } diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 75d2927515eb..38b499b94b84 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -24,9 +24,8 @@ impl TryFrom for Datetime { } } -#[async_trait::async_trait] impl wall_clock::Host for T { - async fn now(&mut self) -> anyhow::Result { + fn now(&mut self) -> anyhow::Result { let now = self.ctx().clocks.wall.now(); Ok(Datetime { seconds: now.as_secs(), @@ -34,7 +33,7 @@ impl wall_clock::Host for T { }) } - async fn resolution(&mut self) -> anyhow::Result { + fn resolution(&mut self) -> anyhow::Result { let res = self.ctx().clocks.wall.resolution(); Ok(Datetime { seconds: res.as_secs(), @@ -43,38 +42,32 @@ impl wall_clock::Host for T { } } -#[async_trait::async_trait] impl monotonic_clock::Host for T { - async fn now(&mut self) -> anyhow::Result { + fn now(&mut self) -> anyhow::Result { Ok(self.ctx().clocks.monotonic.now()) } - async fn resolution(&mut self) -> anyhow::Result { + fn resolution(&mut self) -> anyhow::Result { Ok(self.ctx().clocks.monotonic.resolution()) } - async fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result { + fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result { Ok(self .table_mut() .push(Box::new(PollableEntry::MonotonicClock(when, absolute)))?) } } -#[async_trait::async_trait] impl timezone::Host for T { - async fn display( - &mut self, - timezone: Timezone, - when: Datetime, - ) -> anyhow::Result { + fn display(&mut self, timezone: Timezone, when: Datetime) -> anyhow::Result { todo!() } - async fn utc_offset(&mut self, timezone: Timezone, when: Datetime) -> anyhow::Result { + fn utc_offset(&mut self, timezone: Timezone, when: Datetime) -> anyhow::Result { todo!() } - async fn drop_timezone(&mut self, timezone: Timezone) -> anyhow::Result<()> { + fn drop_timezone(&mut self, timezone: Timezone) -> anyhow::Result<()> { todo!() } } diff --git a/crates/wasi/src/preview2/preview2/env.rs b/crates/wasi/src/preview2/preview2/env.rs index b4288fc37f76..8e6d946844c1 100644 --- a/crates/wasi/src/preview2/preview2/env.rs +++ b/crates/wasi/src/preview2/preview2/env.rs @@ -7,42 +7,35 @@ use crate::preview2::wasi::filesystem::filesystem; use crate::preview2::wasi::io::streams; use crate::preview2::WasiView; -#[async_trait::async_trait] impl environment::Host for T { - async fn get_environment(&mut self) -> anyhow::Result> { + fn get_environment(&mut self) -> anyhow::Result> { Ok(self.ctx().env.clone()) } - async fn get_arguments(&mut self) -> anyhow::Result> { + fn get_arguments(&mut self) -> anyhow::Result> { Ok(self.ctx().args.clone()) } } -#[async_trait::async_trait] impl preopens::Host for T { - async fn get_directories( - &mut self, - ) -> Result, anyhow::Error> { + fn get_directories(&mut self) -> Result, anyhow::Error> { Ok(self.ctx().preopens.clone()) } } -#[async_trait::async_trait] impl stdin::Host for T { - async fn get_stdin(&mut self) -> Result { + fn get_stdin(&mut self) -> Result { Ok(self.ctx().stdin) } } -#[async_trait::async_trait] impl stdout::Host for T { - async fn get_stdout(&mut self) -> Result { + fn get_stdout(&mut self) -> Result { Ok(self.ctx().stdout) } } -#[async_trait::async_trait] impl stderr::Host for T { - async fn get_stderr(&mut self) -> Result { + fn get_stderr(&mut self) -> Result { Ok(self.ctx().stderr) } } diff --git a/crates/wasi/src/preview2/preview2/exit.rs b/crates/wasi/src/preview2/preview2/exit.rs index 45754d594b13..6f008daadecb 100644 --- a/crates/wasi/src/preview2/preview2/exit.rs +++ b/crates/wasi/src/preview2/preview2/exit.rs @@ -1,8 +1,7 @@ use crate::preview2::{wasi::cli_base::exit, I32Exit, WasiView}; -#[async_trait::async_trait] impl exit::Host for T { - async fn exit(&mut self, status: Result<(), ()>) -> anyhow::Result<()> { + fn exit(&mut self, status: Result<(), ()>) -> anyhow::Result<()> { let status = match status { Ok(()) => 0, Err(()) => 1, diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index a2e3135bed25..4b3b84ea892b 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -16,9 +16,8 @@ impl From for filesystem::Error { } } -#[async_trait::async_trait] impl filesystem::Host for T { - async fn advise( + fn advise( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -42,7 +41,7 @@ impl filesystem::Host for T { Ok(()) } - async fn sync_data(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + fn sync_data(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { match table.get_file(fd)?.file.sync_data() { @@ -70,7 +69,7 @@ impl filesystem::Host for T { } } - async fn get_flags( + fn get_flags( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -119,7 +118,7 @@ impl filesystem::Host for T { } } - async fn get_type( + fn get_type( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -135,7 +134,7 @@ impl filesystem::Host for T { } } - async fn set_size( + fn set_size( &mut self, fd: filesystem::Descriptor, size: filesystem::Filesize, @@ -148,7 +147,7 @@ impl filesystem::Host for T { Ok(()) } - async fn set_times( + fn set_times( &mut self, fd: filesystem::Descriptor, atim: filesystem::NewTimestamp, @@ -180,7 +179,7 @@ impl filesystem::Host for T { } } - async fn read( + fn read( &mut self, fd: filesystem::Descriptor, len: filesystem::Filesize, @@ -211,7 +210,7 @@ impl filesystem::Host for T { Ok((buffer, end)) } - async fn write( + fn write( &mut self, fd: filesystem::Descriptor, buf: Vec, @@ -231,7 +230,7 @@ impl filesystem::Host for T { Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) } - async fn read_directory( + fn read_directory( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -286,7 +285,7 @@ impl filesystem::Host for T { Ok(table.push_readdir(ReaddirIterator::new(entries))?) } - async fn read_directory_entry( + fn read_directory_entry( &mut self, stream: filesystem::DirectoryEntryStream, ) -> Result, filesystem::Error> { @@ -295,7 +294,7 @@ impl filesystem::Host for T { readdir.next() } - async fn drop_directory_entry_stream( + fn drop_directory_entry_stream( &mut self, stream: filesystem::DirectoryEntryStream, ) -> anyhow::Result<()> { @@ -303,7 +302,7 @@ impl filesystem::Host for T { Ok(()) } - async fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { match table.get_file(fd)?.file.sync_all() { @@ -331,7 +330,7 @@ impl filesystem::Host for T { } } - async fn create_directory_at( + fn create_directory_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -345,7 +344,7 @@ impl filesystem::Host for T { Ok(()) } - async fn stat( + fn stat( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -365,7 +364,7 @@ impl filesystem::Host for T { } } - async fn stat_at( + fn stat_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -385,7 +384,7 @@ impl filesystem::Host for T { Ok(descriptorstat_from(meta)) } - async fn set_times_at( + fn set_times_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -418,7 +417,7 @@ impl filesystem::Host for T { Ok(()) } - async fn link_at( + fn link_at( &mut self, fd: filesystem::Descriptor, // TODO delete the path flags from this function @@ -443,7 +442,7 @@ impl filesystem::Host for T { Ok(()) } - async fn open_at( + fn open_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -541,7 +540,7 @@ impl filesystem::Host for T { } } - async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { + fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { let table = self.table_mut(); if table.delete_file(fd).is_err() { table.delete_dir(fd)?; @@ -549,7 +548,7 @@ impl filesystem::Host for T { Ok(()) } - async fn readlink_at( + fn readlink_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -566,7 +565,7 @@ impl filesystem::Host for T { .map_err(|_| ErrorCode::IllegalByteSequence)?) } - async fn remove_directory_at( + fn remove_directory_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -579,7 +578,7 @@ impl filesystem::Host for T { Ok(d.dir.remove_dir(&path)?) } - async fn rename_at( + fn rename_at( &mut self, fd: filesystem::Descriptor, old_path: String, @@ -599,7 +598,7 @@ impl filesystem::Host for T { Ok(()) } - async fn symlink_at( + fn symlink_at( &mut self, fd: filesystem::Descriptor, src_path: String, @@ -618,7 +617,7 @@ impl filesystem::Host for T { Ok(()) } - async fn unlink_file_at( + fn unlink_file_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -634,7 +633,7 @@ impl filesystem::Host for T { Ok(()) } - async fn access_at( + fn access_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -644,7 +643,7 @@ impl filesystem::Host for T { todo!() } - async fn change_file_permissions_at( + fn change_file_permissions_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -654,7 +653,7 @@ impl filesystem::Host for T { todo!() } - async fn change_directory_permissions_at( + fn change_directory_permissions_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -664,36 +663,27 @@ impl filesystem::Host for T { todo!() } - async fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!() } - async fn lock_exclusive( - &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { + fn lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!() } - async fn try_lock_shared( - &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { + fn try_lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!() } - async fn try_lock_exclusive( - &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { + fn try_lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!() } - async fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!() } - async fn read_via_stream( + fn read_via_stream( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -716,7 +706,7 @@ impl filesystem::Host for T { Ok(index) } - async fn write_via_stream( + fn write_via_stream( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -740,7 +730,7 @@ impl filesystem::Host for T { Ok(index) } - async fn append_via_stream( + fn append_via_stream( &mut self, fd: filesystem::Descriptor, ) -> Result { diff --git a/crates/wasi/src/preview2/preview2/random.rs b/crates/wasi/src/preview2/preview2/random.rs index 2cabe352b260..03798d4f47f4 100644 --- a/crates/wasi/src/preview2/preview2/random.rs +++ b/crates/wasi/src/preview2/preview2/random.rs @@ -4,37 +4,34 @@ use crate::preview2::wasi::random::random; use crate::preview2::WasiView; use cap_rand::{distributions::Standard, Rng}; -#[async_trait::async_trait] impl random::Host for T { - async fn get_random_bytes(&mut self, len: u64) -> anyhow::Result> { + fn get_random_bytes(&mut self, len: u64) -> anyhow::Result> { Ok((&mut self.ctx_mut().random) .sample_iter(Standard) .take(len as usize) .collect()) } - async fn get_random_u64(&mut self) -> anyhow::Result { + fn get_random_u64(&mut self) -> anyhow::Result { Ok(self.ctx_mut().random.sample(Standard)) } } -#[async_trait::async_trait] impl insecure::Host for T { - async fn get_insecure_random_bytes(&mut self, len: u64) -> anyhow::Result> { + fn get_insecure_random_bytes(&mut self, len: u64) -> anyhow::Result> { Ok((&mut self.ctx_mut().insecure_random) .sample_iter(Standard) .take(len as usize) .collect()) } - async fn get_insecure_random_u64(&mut self) -> anyhow::Result { + fn get_insecure_random_u64(&mut self) -> anyhow::Result { Ok(self.ctx_mut().insecure_random.sample(Standard)) } } -#[async_trait::async_trait] impl insecure_seed::Host for T { - async fn insecure_seed(&mut self) -> anyhow::Result<(u64, u64)> { + fn insecure_seed(&mut self) -> anyhow::Result<(u64, u64)> { let seed: u128 = self.ctx_mut().insecure_random_seed; Ok((seed as u64, (seed >> 64) as u64)) } diff --git a/crates/wasi/src/preview2/wasi/mod.rs b/crates/wasi/src/preview2/wasi/mod.rs index aa1a1708a5c1..5c8599421a6a 100644 --- a/crates/wasi/src/preview2/wasi/mod.rs +++ b/crates/wasi/src/preview2/wasi/mod.rs @@ -1,30 +1,64 @@ pub mod command; -wasmtime::component::bindgen!({ +pub mod sync_io { + pub(crate) mod _internal { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:poll/poll + import wasi:io/streams + ", + tracing: true, + trappable_error_type: { + "streams"::"stream-error": Error, + } + }); + } + pub use self::_internal::wasi::{io, poll}; +} + +pub(crate) mod _internal_io { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:poll/poll + import wasi:io/streams + ", + tracing: true, + async: true, + trappable_error_type: { + "streams"::"stream-error": Error, + } + }); +} +pub use self::_internal_io::wasi::{io, poll}; +pub(crate) mod _internal_rest { + wasmtime::component::bindgen!({ path: "wit", interfaces: " - import wasi:clocks/wall-clock - import wasi:clocks/monotonic-clock - import wasi:clocks/timezone - import wasi:filesystem/filesystem - import wasi:random/random - import wasi:random/insecure - import wasi:random/insecure-seed - import wasi:poll/poll - import wasi:io/streams - import wasi:cli-base/environment - import wasi:cli-base/preopens - import wasi:cli-base/exit - import wasi:cli-base/stdin - import wasi:cli-base/stdout - import wasi:cli-base/stderr - ", + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/filesystem + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:cli-base/environment + import wasi:cli-base/preopens + import wasi:cli-base/exit + import wasi:cli-base/stdin + import wasi:cli-base/stdout + import wasi:cli-base/stderr + ", tracing: true, - async: true, trappable_error_type: { "filesystem"::"error-code": Error, "streams"::"stream-error": Error, + }, + with: { + "wasi:poll/poll": crate::preview2::wasi::poll::poll, + "wasi:io/streams": crate::preview2::wasi::io::streams } -}); - -pub use wasi::*; + }); +} +pub use self::_internal_rest::wasi::*; From d6476659988b0aa30487b9c501762f58b6cebf1e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 1 Jun 2023 16:29:37 -0700 Subject: [PATCH 002/118] streams: get rid of as_any method, which is no longer used --- crates/wasi/src/preview2/filesystem.rs | 12 ------------ crates/wasi/src/preview2/pipe.rs | 8 -------- crates/wasi/src/preview2/stdio.rs | 8 -------- crates/wasi/src/preview2/stream.rs | 5 ----- 4 files changed, 33 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 58971adf5e3a..4c3c98071107 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,5 +1,4 @@ use crate::preview2::{InputStream, OutputStream, Table, TableError}; -use std::any::Any; use std::sync::Arc; bitflags::bitflags! { @@ -97,9 +96,6 @@ impl FileInputStream { #[async_trait::async_trait] impl InputStream for FileInputStream { - fn as_any(&self) -> &dyn Any { - self - } #[cfg(unix)] fn pollable_read(&self) -> Option { use cap_std::io_lifetimes::AsFd; @@ -160,10 +156,6 @@ impl FileOutputStream { #[async_trait::async_trait] impl OutputStream for FileOutputStream { - fn as_any(&self) -> &dyn Any { - self - } - /// If this stream is writing from a host file descriptor, return it so /// that it can be polled with a host poll. #[cfg(unix)] @@ -221,10 +213,6 @@ impl FileAppendStream { #[async_trait::async_trait] impl OutputStream for FileAppendStream { - fn as_any(&self) -> &dyn Any { - self - } - /// If this stream is writing from a host file descriptor, return it so /// that it can be polled with a host poll. #[cfg(unix)] diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index a29aada93895..0a5dd630e360 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -101,10 +101,6 @@ impl From<&str> for ReadPipe> { #[async_trait::async_trait] impl InputStream for ReadPipe { - fn as_any(&self) -> &dyn Any { - self - } - async fn num_ready_bytes(&self) -> Result { Ok(self.borrow().num_ready_bytes()?) } @@ -199,10 +195,6 @@ impl WritePipe>> { #[async_trait::async_trait] impl OutputStream for WritePipe { - fn as_any(&self) -> &dyn Any { - self - } - async fn write(&mut self, buf: &[u8]) -> Result { let n = self.borrow().write(buf)?; Ok(n.try_into()?) diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 7f881eb1cbee..737f21926835 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,5 +1,4 @@ use anyhow::Error; -use std::any::Any; use std::convert::TryInto; use std::io::{self, Read, Write}; use system_interface::io::ReadReady; @@ -20,9 +19,6 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl InputStream for Stdin { - fn as_any(&self) -> &dyn Any { - self - } #[cfg(unix)] fn pollable_read(&self) -> Option { Some(self.0.as_fd()) @@ -94,10 +90,6 @@ macro_rules! wasi_output_stream_impl { ($ty:ty, $ident:ident) => { #[async_trait::async_trait] impl OutputStream for $ty { - fn as_any(&self) -> &dyn Any { - self - } - #[cfg(unix)] fn pollable_write(&self) -> Option { Some(self.0.as_fd()) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 6b5ec86e461e..71f8002490c5 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,6 +1,5 @@ use crate::preview2::{Table, TableError}; use anyhow::Error; -use std::any::Any; /// An input bytestream. /// @@ -9,8 +8,6 @@ use std::any::Any; /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] pub trait InputStream: Send + Sync { - fn as_any(&self) -> &dyn Any; - /// If this stream is reading from a host file descriptor, return it so /// that it can be polled with a host poll. #[cfg(unix)] @@ -79,8 +76,6 @@ pub trait InputStream: Send + Sync { /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] pub trait OutputStream: Send + Sync { - fn as_any(&self) -> &dyn Any; - /// If this stream is writing from a host file descriptor, return it so /// that it can be polled with a host poll. #[cfg(unix)] From 9d04496b722e769b82dee1948575590e598be4f2 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 1 Jun 2023 16:55:51 -0700 Subject: [PATCH 003/118] delete legacy sched and pollable concepts --- crates/wasi/src/preview2/filesystem.rs | 49 ------ crates/wasi/src/preview2/mod.rs | 1 - crates/wasi/src/preview2/pipe.rs | 4 - crates/wasi/src/preview2/preview2/clocks.rs | 4 +- crates/wasi/src/preview2/preview2/io.rs | 5 +- crates/wasi/src/preview2/preview2/poll.rs | 69 +------- crates/wasi/src/preview2/sched.rs | 105 ------------ .../wasi/src/preview2/sched/subscription.rs | 104 ------------ crates/wasi/src/preview2/sched/sync.rs | 156 ------------------ crates/wasi/src/preview2/stdio.rs | 68 -------- crates/wasi/src/preview2/stream.rs | 57 ++----- 11 files changed, 25 insertions(+), 597 deletions(-) delete mode 100644 crates/wasi/src/preview2/sched.rs delete mode 100644 crates/wasi/src/preview2/sched/subscription.rs delete mode 100644 crates/wasi/src/preview2/sched/sync.rs diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 4c3c98071107..a9a1745497b0 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -96,16 +96,6 @@ impl FileInputStream { #[async_trait::async_trait] impl InputStream for FileInputStream { - #[cfg(unix)] - fn pollable_read(&self) -> Option { - use cap_std::io_lifetimes::AsFd; - Some(self.file.as_fd()) - } - #[cfg(windows)] - fn pollable_read(&self) -> Option { - use io_extras::os::windows::AsHandleOrSocket; - Some(self.file.as_handle_or_socket()) - } async fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, bool)> { use system_interface::fs::FileIoExt; let (n, end) = read_result(self.file.read_at(buf, self.position))?; @@ -125,12 +115,7 @@ impl InputStream for FileInputStream { use system_interface::fs::FileIoExt; self.file.is_read_vectored_at() } - async fn num_ready_bytes(&self) -> anyhow::Result { - // FIXME we ought to be able to do better than this - Ok(0) - } async fn readable(&self) -> anyhow::Result<()> { - // FIXME is this the spot to perform the permission check? Ok(()) } } @@ -156,22 +141,6 @@ impl FileOutputStream { #[async_trait::async_trait] impl OutputStream for FileOutputStream { - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(unix)] - fn pollable_write(&self) -> Option { - use cap_std::io_lifetimes::AsFd; - Some(self.file.as_fd()) - } - - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(windows)] - fn pollable_write(&self) -> Option { - use io_extras::os::windows::AsHandleOrSocket; - Some(self.file.as_handle_or_socket()) - } - /// Write bytes. On success, returns the number of bytes written. async fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; @@ -197,7 +166,6 @@ impl OutputStream for FileOutputStream { /// Test whether this stream is writable. async fn writable(&self) -> anyhow::Result<()> { - // FIXME perm check? Ok(()) } } @@ -213,22 +181,6 @@ impl FileAppendStream { #[async_trait::async_trait] impl OutputStream for FileAppendStream { - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(unix)] - fn pollable_write(&self) -> Option { - use cap_std::io_lifetimes::AsFd; - Some(self.file.as_fd()) - } - - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(windows)] - fn pollable_write(&self) -> Option { - use io_extras::os::windows::AsHandleOrSocket; - Some(self.file.as_handle_or_socket()) - } - /// Write bytes. On success, returns the number of bytes written. async fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; @@ -251,7 +203,6 @@ impl OutputStream for FileAppendStream { /// Test whether this stream is writable. async fn writable(&self) -> anyhow::Result<()> { - // FIXME perm check? Ok(()) } } diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 04816de7f279..7b2071116842 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -28,7 +28,6 @@ pub mod pipe; pub mod preview1; pub mod preview2; pub mod random; -mod sched; pub mod stdio; pub mod stream; pub mod table; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 0a5dd630e360..b7600147250b 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -101,10 +101,6 @@ impl From<&str> for ReadPipe> { #[async_trait::async_trait] impl InputStream for ReadPipe { - async fn num_ready_bytes(&self) -> Result { - Ok(self.borrow().num_ready_bytes()?) - } - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { match self.borrow().read(buf) { Ok(0) => Ok((0, true)), diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 38b499b94b84..2037a02fccdd 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -1,6 +1,6 @@ #![allow(unused_variables)] -use crate::preview2::preview2::poll::PollableEntry; +use crate::preview2::preview2::poll::HostPollable; use crate::preview2::wasi::{ clocks::monotonic_clock::{self, Instant}, clocks::timezone::{self, Timezone, TimezoneDisplay}, @@ -54,7 +54,7 @@ impl monotonic_clock::Host for T { fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result { Ok(self .table_mut() - .push(Box::new(PollableEntry::MonotonicClock(when, absolute)))?) + .push(todo!("FIXME integrate HostPollable here!"))?) } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 4e9971f2dc05..4f32ab1a9af2 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -1,5 +1,4 @@ use crate::preview2::{ - preview2::poll::PollableEntry, stream::TableStreamExt, wasi::io::streams::{self, InputStream, OutputStream, StreamError}, wasi::poll::poll::Pollable, @@ -201,7 +200,7 @@ impl streams::Host for T { async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { Ok(self .table_mut() - .push(Box::new(PollableEntry::Read(stream)))?) + .push(todo!("FIXME integrate HostPollable here"))?) } async fn subscribe_to_output_stream( @@ -210,6 +209,6 @@ impl streams::Host for T { ) -> anyhow::Result { Ok(self .table_mut() - .push(Box::new(PollableEntry::Write(stream)))?) + .push(todo!("FIXME integrate HostPollable here"))?) } } diff --git a/crates/wasi/src/preview2/preview2/poll.rs b/crates/wasi/src/preview2/preview2/poll.rs index d3b9380c71bc..d5d24b59b88c 100644 --- a/crates/wasi/src/preview2/preview2/poll.rs +++ b/crates/wasi/src/preview2/preview2/poll.rs @@ -1,83 +1,28 @@ use crate::preview2::{ - stream::TableStreamExt, - wasi::clocks::monotonic_clock::Instant, - wasi::io::streams::{InputStream, OutputStream}, wasi::poll::poll::{self, Pollable}, WasiView, }; +use std::future::Future; +use std::pin::Pin; -/// A pollable resource table entry. -#[derive(Copy, Clone)] -pub(crate) enum PollableEntry { - /// Poll for read events. - Read(InputStream), - /// Poll for write events. - Write(OutputStream), - /// Poll for a monotonic-clock timer. - MonotonicClock(Instant, bool), - /* FIXME: need to rebuild the poll interface to let pollables be created in different crates. - /// Poll for a tcp-socket. - TcpSocket(TcpSocket), - */ -} - -// Implementatations of the interface. The bodies had been pulled out into -// functions above to allow them to be shared between the two worlds, which -// used to require different traits . Features have been added to facilitate -// sharing between worlds, but I want to avoid the huge whitespace diff on -// this PR. +pub struct HostPollable(Pin>>); #[async_trait::async_trait] impl poll::Host for T { async fn drop_pollable(&mut self, pollable: Pollable) -> anyhow::Result<()> { - self.table_mut().delete::(pollable)?; + self.table_mut().delete::(pollable)?; Ok(()) } async fn poll_oneoff(&mut self, futures: Vec) -> anyhow::Result> { - use crate::preview2::sched::{sync::SyncSched, Poll, Userdata, WasiSched}; - // Convert `futures` into `Poll` subscriptions. - let mut poll = Poll::new(); let len = futures.len(); - for (index, future) in futures.into_iter().enumerate() { - let userdata = Userdata::from(index as u64); - - match *self.table().get(future)? { - PollableEntry::Read(stream) => { - let wasi_stream: &dyn crate::preview2::InputStream = - self.table().get_input_stream(stream)?; - poll.subscribe_read(wasi_stream, userdata); - } - PollableEntry::Write(stream) => { - let wasi_stream: &dyn crate::preview2::OutputStream = - self.table().get_output_stream(stream)?; - poll.subscribe_write(wasi_stream, userdata); - } - PollableEntry::MonotonicClock(when, absolute) => { - poll.subscribe_monotonic_clock( - &*self.ctx().clocks.monotonic, - when, - absolute, - userdata, - ); - } /* - PollableEntry::TcpSocket(tcp_socket) => { - let wasi_tcp_socket: &dyn crate::WasiTcpSocket = - self.table().get_tcp_socket(tcp_socket)?; - poll.subscribe_tcp_socket(wasi_tcp_socket, userdata); - } - */ - } - } - - // Do the poll. - SyncSched.poll_oneoff(&mut poll).await?; // Convert the results into a list of `u8` to return. let mut results = vec![0_u8; len]; - for (_result, data) in poll.results() { - results[u64::from(data) as usize] = u8::from(true); + if todo!() { + let index: usize = todo!(); + results[index] = u8::from(true); } Ok(results) } diff --git a/crates/wasi/src/preview2/sched.rs b/crates/wasi/src/preview2/sched.rs deleted file mode 100644 index eab79e1e57b3..000000000000 --- a/crates/wasi/src/preview2/sched.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![allow(dead_code)] -use crate::preview2::{ - clocks::WasiMonotonicClock, - stream::{InputStream, OutputStream}, -}; -use anyhow::Error; -pub(crate) mod subscription; -pub(crate) mod sync; -pub use cap_std::time::Duration; - -pub(crate) use subscription::{ - MonotonicClockSubscription, RwSubscription, Subscription, SubscriptionResult, -}; - -#[async_trait::async_trait] -pub(crate) trait WasiSched: Send + Sync { - async fn poll_oneoff<'a>(&self, poll: &mut Poll<'a>) -> Result<(), Error>; - async fn sched_yield(&self) -> Result<(), Error>; - async fn sleep(&self, duration: Duration) -> Result<(), Error>; -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) struct Userdata(u64); -impl From for Userdata { - fn from(u: u64) -> Userdata { - Userdata(u) - } -} - -impl From for u64 { - fn from(u: Userdata) -> u64 { - u.0 - } -} - -pub(crate) struct Poll<'a> { - subs: Vec<(Subscription<'a>, Userdata)>, -} - -impl<'a> Poll<'a> { - pub fn new() -> Self { - Self { subs: Vec::new() } - } - pub fn subscribe_monotonic_clock( - &mut self, - clock: &'a dyn WasiMonotonicClock, - deadline: u64, - absolute: bool, - ud: Userdata, - ) { - let deadline = if absolute { - // Convert an absolute deadline to a relative one. - deadline.saturating_sub(clock.now()) - } else { - deadline - }; - self.subs.push(( - Subscription::MonotonicClock(MonotonicClockSubscription { clock, deadline }), - ud, - )); - } - pub fn subscribe_read(&mut self, stream: &'a dyn InputStream, ud: Userdata) { - self.subs.push(( - Subscription::ReadWrite(RwSubscription::new_input(stream)), - ud, - )); - } - pub fn subscribe_write(&mut self, stream: &'a dyn OutputStream, ud: Userdata) { - self.subs.push(( - Subscription::ReadWrite(RwSubscription::new_output(stream)), - ud, - )); - } - /* FIXME need to redo poll interface to support pollables defined in other crates - pub fn subscribe_tcp_socket(&mut self, tcp_socket: &'a dyn WasiTcpSocket, ud: Userdata) { - self.subs.push(( - Subscription::ReadWrite(RwSubscription::new_tcp_socket(tcp_socket)), - ud, - )); - } - */ - pub fn results(self) -> impl Iterator + 'a { - self.subs - .into_iter() - .filter_map(|(s, ud)| SubscriptionResult::from_subscription(s).map(|r| (r, ud))) - } - pub fn is_empty(&self) -> bool { - self.subs.is_empty() - } - pub fn earliest_clock_deadline(&self) -> Option<&MonotonicClockSubscription<'a>> { - self.subs - .iter() - .filter_map(|(s, _ud)| match s { - Subscription::MonotonicClock(t) => Some(t), - _ => None, - }) - .min_by(|a, b| a.deadline.cmp(&b.deadline)) - } - pub fn rw_subscriptions<'b>(&'b mut self) -> impl Iterator> { - self.subs.iter_mut().filter_map(|sub| match &mut sub.0 { - Subscription::ReadWrite(rwsub) => Some(rwsub), - _ => None, - }) - } -} diff --git a/crates/wasi/src/preview2/sched/subscription.rs b/crates/wasi/src/preview2/sched/subscription.rs deleted file mode 100644 index 20a1d9aa07ff..000000000000 --- a/crates/wasi/src/preview2/sched/subscription.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::preview2::{ - clocks::WasiMonotonicClock, - stream::{InputStream, OutputStream}, -}; -use anyhow::Error; -use bitflags::bitflags; - -bitflags! { - pub struct RwEventFlags: u32 { - const HANGUP = 0b1; - } -} - -pub enum RwStream<'a> { - // fixme: rename? - Read(&'a dyn InputStream), - Write(&'a dyn OutputStream), - /* - TcpSocket(&'a dyn WasiTcpSocket), - */ -} - -pub struct RwSubscription<'a> { - pub stream: RwStream<'a>, - status: Option>, -} - -impl<'a> RwSubscription<'a> { - pub fn new_input(stream: &'a dyn InputStream) -> Self { - Self { - stream: RwStream::Read(stream), - status: None, - } - } - pub fn new_output(stream: &'a dyn OutputStream) -> Self { - Self { - stream: RwStream::Write(stream), - status: None, - } - } - /* - pub fn new_tcp_socket(tcp_socket: &'a dyn WasiTcpSocket) -> Self { - Self { - stream: RwStream::TcpSocket(tcp_socket), - status: None, - } - } - */ - pub fn complete(&mut self, flags: RwEventFlags) { - self.status = Some(Ok(flags)) - } - pub fn error(&mut self, error: Error) { - self.status = Some(Err(error)) - } - pub fn result(&mut self) -> Option> { - self.status.take() - } - pub fn is_complete(&self) -> bool { - self.status.is_some() - } -} - -pub struct MonotonicClockSubscription<'a> { - pub clock: &'a dyn WasiMonotonicClock, - pub deadline: u64, -} - -impl<'a> MonotonicClockSubscription<'a> { - pub fn now(&self) -> u64 { - self.clock.now() - } - pub fn duration_until(&self) -> Option { - self.deadline.checked_sub(self.now()) - } - pub fn result(&self) -> Option> { - if self.now().checked_sub(self.deadline).is_some() { - Some(Ok(())) - } else { - None - } - } -} - -pub enum Subscription<'a> { - ReadWrite(RwSubscription<'a>), - MonotonicClock(MonotonicClockSubscription<'a>), -} - -#[derive(Debug)] -pub enum SubscriptionResult { - ReadWrite(Result), - MonotonicClock(Result<(), Error>), -} - -impl SubscriptionResult { - pub fn from_subscription(s: Subscription) -> Option { - match s { - Subscription::ReadWrite(mut s) => { - s.result().map(|sub| SubscriptionResult::ReadWrite(sub)) - } - Subscription::MonotonicClock(s) => s.result().map(SubscriptionResult::MonotonicClock), - } - } -} diff --git a/crates/wasi/src/preview2/sched/sync.rs b/crates/wasi/src/preview2/sched/sync.rs deleted file mode 100644 index 431db5c774e8..000000000000 --- a/crates/wasi/src/preview2/sched/sync.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::preview2::sched::{ - subscription::{RwEventFlags, RwStream}, - Poll, WasiSched, -}; -use rustix::io::{PollFd, PollFlags}; -use std::thread; -use std::time::Duration; - -use anyhow::Error; - -pub(crate) async fn poll_oneoff<'a>(poll: &mut Poll<'a>) -> Result<(), Error> { - // Collect all stream I/O subscriptions. Clock subscriptions are handled - // separately below. - let mut ready = false; - let mut pollfds = Vec::new(); - for rwsub in poll.rw_subscriptions() { - match rwsub.stream { - RwStream::Read(stream) => { - // Poll things that can be polled. - if let Some(fd) = stream.pollable_read() { - #[cfg(unix)] - { - pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::IN)); - continue; - } - - #[cfg(windows)] - { - if let Some(fd) = fd.as_socket() { - pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::IN)); - continue; - } - } - } - - // Allow in-memory buffers or other immediately-available - // sources to complete successfully. - if let Ok(nbytes) = stream.num_ready_bytes().await { - if nbytes != 0 { - rwsub.complete(RwEventFlags::empty()); - ready = true; - continue; - } - } - - return Err(anyhow::anyhow!("stream is not pollable for reading")); - } - - RwStream::Write(stream) => { - let fd = stream - .pollable_write() - .ok_or_else(|| anyhow::anyhow!("stream is not pollable for writing"))?; - - #[cfg(unix)] - { - pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::OUT)); - } - - #[cfg(windows)] - { - if let Some(fd) = fd.as_socket() { - pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::OUT)); - } else { - return Err(anyhow::anyhow!( - "unimplemented: polling for writing to non-OS resources" - )); - } - } - } /* FIXME redesign of sched to make it possible to define pollables out of crate - RwStream::TcpSocket(tcp_socket) => { - let fd = tcp_socket.pollable(); - pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::IN | PollFlags::PRI)); - } - */ - } - } - - // If we didn't have any streams that are immediately available, do an OS - // `poll` to wait for streams to become available. - if !ready { - loop { - let poll_timeout = if let Some(t) = poll.earliest_clock_deadline() { - // Convert the timeout to milliseconds for `poll`, rounding up. - // - // TODO: On Linux and FreeBSD, we could use `ppoll` instead - // which takes a `timespec.` - ((t.deadline + 999_999) / 1_000_000) - .try_into() - .map_err(|_| anyhow::anyhow!("overflow: poll timeout"))? - } else { - // A negative value requests an infinite timeout. - -1 - }; - tracing::debug!( - poll_timeout = tracing::field::debug(poll_timeout), - poll_fds = tracing::field::debug(&pollfds), - "poll" - ); - match rustix::io::poll(&mut pollfds, poll_timeout) { - Ok(_num_ready) => { - ready = true; - break; - } - Err(rustix::io::Errno::INTR) => continue, - Err(err) => return Err(std::io::Error::from(err).into()), - } - } - - assert_eq!(poll.rw_subscriptions().count(), pollfds.len()); - - // If the OS `poll` returned events, record them. - if ready { - // Iterate through the stream subscriptions, skipping those that - // were already completed due to being immediately available. - for (rwsub, pollfd) in poll.rw_subscriptions().zip(pollfds.into_iter()) { - let revents = pollfd.revents(); - if revents.contains(PollFlags::NVAL) { - rwsub.error(anyhow::anyhow!("rw subscription badf")); - } else if revents.contains(PollFlags::ERR) { - rwsub.error(anyhow::anyhow!("rw subscription io error")); - } else if revents.contains(PollFlags::HUP) { - rwsub.complete(RwEventFlags::HANGUP); - } else { - rwsub.complete(RwEventFlags::empty()); - }; - } - } - }; - - // If we had no immediately-available events and no events becoming - // available in a `poll`, it means we timed out. Report that event. - if !ready { - poll.earliest_clock_deadline() - .expect("timed out") - .result() - .expect("timer deadline is past") - .unwrap() - } - - Ok(()) -} -pub(crate) struct SyncSched; -#[async_trait::async_trait] -impl WasiSched for SyncSched { - async fn poll_oneoff<'a>(&self, poll: &mut Poll<'a>) -> Result<(), Error> { - poll_oneoff(poll).await - } - async fn sched_yield(&self) -> Result<(), Error> { - thread::yield_now(); - Ok(()) - } - async fn sleep(&self, duration: Duration) -> Result<(), Error> { - std::thread::sleep(duration); - Ok(()) - } -} diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 737f21926835..e42cd8f3ee3e 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,15 +1,8 @@ use anyhow::Error; use std::convert::TryInto; use std::io::{self, Read, Write}; -use system_interface::io::ReadReady; use crate::preview2::{InputStream, OutputStream}; -#[cfg(unix)] -use cap_std::io_lifetimes::{AsFd, BorrowedFd}; -#[cfg(windows)] -use cap_std::io_lifetimes::{AsHandle, BorrowedHandle}; -#[cfg(windows)] -use io_extras::os::windows::{AsHandleOrSocket, BorrowedHandleOrSocket}; pub struct Stdin(std::io::Stdin); @@ -19,16 +12,6 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl InputStream for Stdin { - #[cfg(unix)] - fn pollable_read(&self) -> Option { - Some(self.0.as_fd()) - } - - #[cfg(windows)] - fn pollable_read(&self) -> Option { - Some(self.0.as_handle_or_socket()) - } - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { match Read::read(&mut self.0, buf) { Ok(0) => Ok((0, true)), @@ -58,47 +41,15 @@ impl InputStream for Stdin { Ok((num, num < nelem)) } - async fn num_ready_bytes(&self) -> Result { - Ok(self.0.num_ready_bytes()?) - } - async fn readable(&self) -> Result<(), Error> { Err(anyhow::anyhow!("idk")) } } -#[cfg(windows)] -impl AsHandle for Stdin { - fn as_handle(&self) -> BorrowedHandle<'_> { - self.0.as_handle() - } -} -#[cfg(windows)] -impl AsHandleOrSocket for Stdin { - #[inline] - fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket { - self.0.as_handle_or_socket() - } -} -#[cfg(unix)] -impl AsFd for Stdin { - fn as_fd(&self) -> BorrowedFd<'_> { - self.0.as_fd() - } -} macro_rules! wasi_output_stream_impl { ($ty:ty, $ident:ident) => { #[async_trait::async_trait] impl OutputStream for $ty { - #[cfg(unix)] - fn pollable_write(&self) -> Option { - Some(self.0.as_fd()) - } - #[cfg(windows)] - fn pollable_write(&self) -> Option { - Some(self.0.as_handle_or_socket()) - } - async fn write(&mut self, buf: &[u8]) -> Result { let n = Write::write(&mut self.0, buf)?; Ok(n.try_into()?) @@ -131,25 +82,6 @@ macro_rules! wasi_output_stream_impl { Ok(()) } } - #[cfg(windows)] - impl AsHandle for $ty { - fn as_handle(&self) -> BorrowedHandle<'_> { - self.0.as_handle() - } - } - #[cfg(unix)] - impl AsFd for $ty { - fn as_fd(&self) -> BorrowedFd<'_> { - self.0.as_fd() - } - } - #[cfg(windows)] - impl AsHandleOrSocket for $ty { - #[inline] - fn as_handle_or_socket(&self) -> BorrowedHandleOrSocket { - self.0.as_handle_or_socket() - } - } }; } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 71f8002490c5..c7de70177783 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -8,32 +8,20 @@ use anyhow::Error; /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] pub trait InputStream: Send + Sync { - /// If this stream is reading from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(unix)] - fn pollable_read(&self) -> Option { - None - } - - /// If this stream is reading from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(windows)] - fn pollable_read(&self) -> Option { - None - } - /// Read bytes. On success, returns a pair holding the number of bytes read /// and a flag indicating whether the end of the stream was reached. - async fn read(&mut self, _buf: &mut [u8]) -> Result<(u64, bool), Error> { - Err(anyhow::anyhow!("badf")) - } + async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error>; /// Vectored-I/O form of `read`. async fn read_vectored<'a>( &mut self, - _bufs: &mut [std::io::IoSliceMut<'a>], + bufs: &mut [std::io::IoSliceMut<'a>], ) -> Result<(u64, bool), Error> { - Err(anyhow::anyhow!("badf")) + if bufs.len() > 0 { + self.read(bufs.get_mut(0).unwrap()).await + } else { + self.read(&mut []).await + } } /// Test whether vectored I/O reads are known to be optimized in the @@ -60,11 +48,6 @@ pub trait InputStream: Send + Sync { Ok((nread, saw_end)) } - /// Return the number of bytes that may be read without blocking. - async fn num_ready_bytes(&self) -> Result { - Ok(0) - } - /// Test whether this stream is readable. async fn readable(&self) -> Result<(), Error>; } @@ -76,28 +59,16 @@ pub trait InputStream: Send + Sync { /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] pub trait OutputStream: Send + Sync { - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(unix)] - fn pollable_write(&self) -> Option { - None - } - - /// If this stream is writing from a host file descriptor, return it so - /// that it can be polled with a host poll. - #[cfg(windows)] - fn pollable_write(&self) -> Option { - None - } - /// Write bytes. On success, returns the number of bytes written. - async fn write(&mut self, _buf: &[u8]) -> Result { - Err(anyhow::anyhow!("badf")) - } + async fn write(&mut self, _buf: &[u8]) -> Result; /// Vectored-I/O form of `write`. - async fn write_vectored<'a>(&mut self, _bufs: &[std::io::IoSlice<'a>]) -> Result { - Err(anyhow::anyhow!("badf")) + async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { + if bufs.len() > 0 { + self.write(bufs.get(0).unwrap()).await + } else { + Ok(0) + } } /// Test whether vectored I/O writes are known to be optimized in the From cda675b99033c78388261560e280d27d75367a98 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 1 Jun 2023 17:19:18 -0700 Subject: [PATCH 004/118] more code motion and renaming --- crates/wasi/src/preview2/ctx.rs | 14 ++--- crates/wasi/src/preview2/filesystem.rs | 8 +-- crates/wasi/src/preview2/mod.rs | 14 +++-- crates/wasi/src/preview2/pipe.rs | 6 +- .../wasi/src/preview2/{preview2 => }/poll.rs | 0 crates/wasi/src/preview2/preview2/clocks.rs | 3 +- crates/wasi/src/preview2/preview2/io.rs | 18 +++--- crates/wasi/src/preview2/preview2/mod.rs | 1 - crates/wasi/src/preview2/stdio.rs | 6 +- crates/wasi/src/preview2/stream.rs | 55 ++++++++++++------- 10 files changed, 69 insertions(+), 56 deletions(-) rename crates/wasi/src/preview2/{preview2 => }/poll.rs (100%) diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 511008b9cb74..4cb322409508 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -2,16 +2,16 @@ use crate::preview2::{ clocks::{self, WasiClocks}, filesystem::{Dir, TableFsExt}, pipe, random, stdio, - stream::{InputStream, OutputStream, TableStreamExt}, + stream::{HostInputStream, HostOutputStream, TableStreamExt}, DirPerms, FilePerms, Table, }; use cap_rand::{Rng, RngCore, SeedableRng}; #[derive(Default)] pub struct WasiCtxBuilder { - stdin: Option>, - stdout: Option>, - stderr: Option>, + stdin: Option>, + stdout: Option>, + stderr: Option>, env: Vec<(String, String)>, args: Vec, preopens: Vec<(Dir, String)>, @@ -45,17 +45,17 @@ impl WasiCtxBuilder { result } - pub fn set_stdin(mut self, stdin: impl InputStream + 'static) -> Self { + pub fn set_stdin(mut self, stdin: impl HostInputStream + 'static) -> Self { self.stdin = Some(Box::new(stdin)); self } - pub fn set_stdout(mut self, stdout: impl OutputStream + 'static) -> Self { + pub fn set_stdout(mut self, stdout: impl HostOutputStream + 'static) -> Self { self.stdout = Some(Box::new(stdout)); self } - pub fn set_stderr(mut self, stderr: impl OutputStream + 'static) -> Self { + pub fn set_stderr(mut self, stderr: impl HostOutputStream + 'static) -> Self { self.stderr = Some(Box::new(stderr)); self } diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index a9a1745497b0..0615a3dc29e0 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,4 +1,4 @@ -use crate::preview2::{InputStream, OutputStream, Table, TableError}; +use crate::preview2::{HostInputStream, HostOutputStream, Table, TableError}; use std::sync::Arc; bitflags::bitflags! { @@ -95,7 +95,7 @@ impl FileInputStream { } #[async_trait::async_trait] -impl InputStream for FileInputStream { +impl HostInputStream for FileInputStream { async fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, bool)> { use system_interface::fs::FileIoExt; let (n, end) = read_result(self.file.read_at(buf, self.position))?; @@ -140,7 +140,7 @@ impl FileOutputStream { } #[async_trait::async_trait] -impl OutputStream for FileOutputStream { +impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. async fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; @@ -180,7 +180,7 @@ impl FileAppendStream { } #[async_trait::async_trait] -impl OutputStream for FileAppendStream { +impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. async fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 7b2071116842..ef49710eb473 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -24,6 +24,7 @@ mod ctx; mod error; pub(crate) mod filesystem; pub mod pipe; +mod poll; #[cfg(feature = "preview1-on-preview2")] pub mod preview1; pub mod preview2; @@ -33,11 +34,12 @@ pub mod stream; pub mod table; pub mod wasi; +pub use self::clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; +pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; +pub use self::error::I32Exit; +pub use self::filesystem::{DirPerms, FilePerms}; +pub use self::poll::HostPollable; +pub use self::stream::{HostInputStream, HostOutputStream}; +pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; -pub use clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; -pub use ctx::{WasiCtx, WasiCtxBuilder, WasiView}; -pub use error::I32Exit; -pub use filesystem::{DirPerms, FilePerms}; -pub use stream::{InputStream, OutputStream}; -pub use table::{Table, TableError}; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index b7600147250b..1d0035f159ac 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -7,7 +7,7 @@ //! Some convenience constructors are included for common backing types like `Vec` and `String`, //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! -use crate::preview2::stream::{InputStream, OutputStream}; +use crate::preview2::stream::{HostInputStream, HostOutputStream}; use anyhow::Error; use std::any::Any; use std::convert::TryInto; @@ -100,7 +100,7 @@ impl From<&str> for ReadPipe> { } #[async_trait::async_trait] -impl InputStream for ReadPipe { +impl HostInputStream for ReadPipe { async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { match self.borrow().read(buf) { Ok(0) => Ok((0, true)), @@ -190,7 +190,7 @@ impl WritePipe>> { } #[async_trait::async_trait] -impl OutputStream for WritePipe { +impl HostOutputStream for WritePipe { async fn write(&mut self, buf: &[u8]) -> Result { let n = self.borrow().write(buf)?; Ok(n.try_into()?) diff --git a/crates/wasi/src/preview2/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs similarity index 100% rename from crates/wasi/src/preview2/preview2/poll.rs rename to crates/wasi/src/preview2/poll.rs diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 2037a02fccdd..01a7c342b68d 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -1,13 +1,12 @@ #![allow(unused_variables)] -use crate::preview2::preview2::poll::HostPollable; use crate::preview2::wasi::{ clocks::monotonic_clock::{self, Instant}, clocks::timezone::{self, Timezone, TimezoneDisplay}, clocks::wall_clock::{self, Datetime}, poll::poll::Pollable, }; -use crate::preview2::WasiView; +use crate::preview2::{HostPollable, WasiView}; use cap_std::time::SystemTime; impl TryFrom for Datetime { diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 4f32ab1a9af2..003db7a4b18f 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -1,5 +1,5 @@ use crate::preview2::{ - stream::TableStreamExt, + stream::{HostInputStream, HostOutputStream, TableStreamExt}, wasi::io::streams::{self, InputStream, OutputStream, StreamError}, wasi::poll::poll::Pollable, TableError, WasiView, @@ -31,13 +31,13 @@ impl From for streams::Error { impl streams::Host for T { async fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { self.table_mut() - .delete::>(stream)?; + .delete::>(stream)?; Ok(()) } async fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { self.table_mut() - .delete::>(stream)?; + .delete::>(stream)?; Ok(()) } @@ -46,8 +46,7 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, bool), streams::Error> { - let s: &mut Box = - self.table_mut().get_input_stream_mut(stream)?; + let s = self.table_mut().get_input_stream_mut(stream)?; // Len could be any `u64` value, but we don't want to // allocate too much up front, so make a wild guess @@ -72,8 +71,7 @@ impl streams::Host for T { } async fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { - let s: &mut Box = - self.table_mut().get_output_stream_mut(stream)?; + let s = self.table_mut().get_output_stream_mut(stream)?; let bytes_written: u64 = s.write(&bytes).await?; @@ -90,8 +88,7 @@ impl streams::Host for T { } async fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { - let s: &mut Box = - self.table_mut().get_input_stream_mut(stream)?; + let s = self.table_mut().get_input_stream_mut(stream)?; let (bytes_skipped, end) = s.skip(len).await?; @@ -112,8 +109,7 @@ impl streams::Host for T { stream: OutputStream, len: u64, ) -> Result { - let s: &mut Box = - self.table_mut().get_output_stream_mut(stream)?; + let s = self.table_mut().get_output_stream_mut(stream)?; let bytes_written: u64 = s.write_zeroes(len).await?; diff --git a/crates/wasi/src/preview2/preview2/mod.rs b/crates/wasi/src/preview2/preview2/mod.rs index 9cb84880eaa5..8bb111cc3a21 100644 --- a/crates/wasi/src/preview2/preview2/mod.rs +++ b/crates/wasi/src/preview2/preview2/mod.rs @@ -3,5 +3,4 @@ mod env; mod exit; pub(crate) mod filesystem; mod io; -mod poll; mod random; diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index e42cd8f3ee3e..4214e38a6dbd 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,7 +2,7 @@ use anyhow::Error; use std::convert::TryInto; use std::io::{self, Read, Write}; -use crate::preview2::{InputStream, OutputStream}; +use crate::preview2::{HostInputStream, HostOutputStream}; pub struct Stdin(std::io::Stdin); @@ -11,7 +11,7 @@ pub fn stdin() -> Stdin { } #[async_trait::async_trait] -impl InputStream for Stdin { +impl HostInputStream for Stdin { async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { match Read::read(&mut self.0, buf) { Ok(0) => Ok((0, true)), @@ -49,7 +49,7 @@ impl InputStream for Stdin { macro_rules! wasi_output_stream_impl { ($ty:ty, $ident:ident) => { #[async_trait::async_trait] - impl OutputStream for $ty { + impl HostOutputStream for $ty { async fn write(&mut self, buf: &[u8]) -> Result { let n = Write::write(&mut self.0, buf)?; Ok(n.try_into()?) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index c7de70177783..0909e224fb84 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -7,7 +7,7 @@ use anyhow::Error; /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] -pub trait InputStream: Send + Sync { +pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes read /// and a flag indicating whether the end of the stream was reached. async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error>; @@ -58,7 +58,7 @@ pub trait InputStream: Send + Sync { /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. #[async_trait::async_trait] -pub trait OutputStream: Send + Sync { +pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. async fn write(&mut self, _buf: &[u8]) -> Result; @@ -80,7 +80,7 @@ pub trait OutputStream: Send + Sync { /// Transfer bytes directly from an input stream to an output stream. async fn splice( &mut self, - src: &mut dyn InputStream, + src: &mut dyn HostInputStream, nelem: u64, ) -> Result<(u64, bool), Error> { let mut nspliced = 0; @@ -122,33 +122,50 @@ pub trait OutputStream: Send + Sync { } pub trait TableStreamExt { - fn push_input_stream(&mut self, istream: Box) -> Result; - fn get_input_stream(&self, fd: u32) -> Result<&dyn InputStream, TableError>; - fn get_input_stream_mut(&mut self, fd: u32) -> Result<&mut Box, TableError>; + fn push_input_stream(&mut self, istream: Box) -> Result; + fn get_input_stream(&self, fd: u32) -> Result<&dyn HostInputStream, TableError>; + fn get_input_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut Box, TableError>; - fn push_output_stream(&mut self, ostream: Box) -> Result; - fn get_output_stream(&self, fd: u32) -> Result<&dyn OutputStream, TableError>; - fn get_output_stream_mut(&mut self, fd: u32) -> Result<&mut Box, TableError>; + fn push_output_stream(&mut self, ostream: Box) + -> Result; + fn get_output_stream(&self, fd: u32) -> Result<&dyn HostOutputStream, TableError>; + fn get_output_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut Box, TableError>; } impl TableStreamExt for Table { - fn push_input_stream(&mut self, istream: Box) -> Result { + fn push_input_stream(&mut self, istream: Box) -> Result { self.push(Box::new(istream)) } - fn get_input_stream(&self, fd: u32) -> Result<&dyn InputStream, TableError> { - self.get::>(fd).map(|f| f.as_ref()) + fn get_input_stream(&self, fd: u32) -> Result<&dyn HostInputStream, TableError> { + self.get::>(fd).map(|f| f.as_ref()) } - fn get_input_stream_mut(&mut self, fd: u32) -> Result<&mut Box, TableError> { - self.get_mut::>(fd) + fn get_input_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut Box, TableError> { + self.get_mut::>(fd) } - fn push_output_stream(&mut self, ostream: Box) -> Result { + fn push_output_stream( + &mut self, + ostream: Box, + ) -> Result { self.push(Box::new(ostream)) } - fn get_output_stream(&self, fd: u32) -> Result<&dyn OutputStream, TableError> { - self.get::>(fd).map(|f| f.as_ref()) + fn get_output_stream(&self, fd: u32) -> Result<&dyn HostOutputStream, TableError> { + self.get::>(fd) + .map(|f| f.as_ref()) } - fn get_output_stream_mut(&mut self, fd: u32) -> Result<&mut Box, TableError> { - self.get_mut::>(fd) + fn get_output_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut Box, TableError> { + self.get_mut::>(fd) } } From 8233f7ccc7714853e69250ab4a9c7ac5b0865f64 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 8 Jun 2023 12:55:31 -0700 Subject: [PATCH 005/118] make tokio a workspace dep, because we need it directly in wasmtime-wasi --- Cargo.lock | 1 + Cargo.toml | 1 + crates/wasi-common/tokio/Cargo.toml | 4 ++-- crates/wasi/Cargo.toml | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf2c4bf79bbf..a3faf149ea6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4239,6 +4239,7 @@ dependencies = [ "rustix", "system-interface", "thiserror", + "tokio", "tracing", "wasi-cap-std-sync", "wasi-common", diff --git a/Cargo.toml b/Cargo.toml index a6c5a7024b55..82cc5235afe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,6 +239,7 @@ tempfile = "3.1.0" filecheck = "0.5.0" libc = "0.2.60" file-per-thread-logger = "0.2.0" +tokio = { version = "1.26.0" } [features] default = [ diff --git a/crates/wasi-common/tokio/Cargo.toml b/crates/wasi-common/tokio/Cargo.toml index 77130b9b8f91..3d0e1be45563 100644 --- a/crates/wasi-common/tokio/Cargo.toml +++ b/crates/wasi-common/tokio/Cargo.toml @@ -14,7 +14,7 @@ include = ["src/**/*", "LICENSE" ] wasi-common = { workspace = true } wasi-cap-std-sync = { workspace = true } wiggle = { workspace = true } -tokio = { version = "1.8.0", features = [ "rt", "fs", "time", "io-util", "net", "io-std", "rt-multi-thread"] } +tokio = { workspace = true, features = [ "rt", "fs", "time", "io-util", "net", "io-std", "rt-multi-thread"] } cap-std = { workspace = true } anyhow = { workspace = true } io-lifetimes = { workspace = true } @@ -27,5 +27,5 @@ io-extras = "0.17.0" [dev-dependencies] tempfile = "3.1.0" -tokio = { version = "1.8.0", features = [ "macros" ] } +tokio = { workspace = true, features = [ "macros" ] } cap-tempfile = "1.0.0" diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 31ac0bf462af..d4cece5e3cad 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -21,6 +21,7 @@ wasi-tokio = { workspace = true, optional = true } wiggle = { workspace = true, optional = true } libc = { workspace = true } +tokio = { workspace = true, optional = true, features = ["time"] } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } @@ -59,6 +60,7 @@ preview2 = [ 'dep:async-trait', 'dep:system-interface', 'dep:rustix', + 'dep:tokio', ] preview1-on-preview2 = [ "preview2", From 0f8f79ea1a19dd61546331f39b53157ae4933562 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 8 Jun 2023 13:43:00 -0700 Subject: [PATCH 006/118] HostPollable exists --- crates/wasi/src/preview2/filesystem.rs | 16 ++-- crates/wasi/src/preview2/mod.rs | 4 +- crates/wasi/src/preview2/pipe.rs | 10 +-- crates/wasi/src/preview2/poll.rs | 89 +++++++++++++++++---- crates/wasi/src/preview2/preview2/clocks.rs | 6 +- crates/wasi/src/preview2/preview2/io.rs | 6 +- crates/wasi/src/preview2/stdio.rs | 12 +-- crates/wasi/src/preview2/stream.rs | 10 +-- 8 files changed, 108 insertions(+), 45 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 0615a3dc29e0..5d9c67c3cbe6 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,4 +1,4 @@ -use crate::preview2::{HostInputStream, HostOutputStream, Table, TableError}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, Table, TableError}; use std::sync::Arc; bitflags::bitflags! { @@ -115,8 +115,8 @@ impl HostInputStream for FileInputStream { use system_interface::fs::FileIoExt; self.file.is_read_vectored_at() } - async fn readable(&self) -> anyhow::Result<()> { - Ok(()) + fn pollable(&self) -> HostPollable { + HostPollable::new(async {}) // FIXME } } @@ -164,9 +164,8 @@ impl HostOutputStream for FileOutputStream { self.file.is_write_vectored_at() } - /// Test whether this stream is writable. - async fn writable(&self) -> anyhow::Result<()> { - Ok(()) + fn pollable(&self) -> HostPollable { + HostPollable::new(async {}) // FIXME } } @@ -201,8 +200,7 @@ impl HostOutputStream for FileAppendStream { self.file.is_write_vectored_at() } - /// Test whether this stream is writable. - async fn writable(&self) -> anyhow::Result<()> { - Ok(()) + fn pollable(&self) -> HostPollable { + HostPollable::new(async {}) // FIXME } } diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index ef49710eb473..8ac170307b1d 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -38,8 +38,8 @@ pub use self::clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; -pub use self::poll::HostPollable; -pub use self::stream::{HostInputStream, HostOutputStream}; +pub use self::poll::{HostPollable, TablePollableExt}; +pub use self::stream::{HostInputStream, HostOutputStream, TableStreamExt}; pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 1d0035f159ac..95f362019838 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -7,7 +7,7 @@ //! Some convenience constructors are included for common backing types like `Vec` and `String`, //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! -use crate::preview2::stream::{HostInputStream, HostOutputStream}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; use anyhow::Error; use std::any::Any; use std::convert::TryInto; @@ -118,8 +118,8 @@ impl HostInputStream for ReadPipe { Ok((num, num < nelem)) } - async fn readable(&self) -> Result<(), Error> { - Ok(()) + fn pollable(&self) -> HostPollable { + HostPollable::new(async {}) // FIXME } } @@ -215,7 +215,7 @@ impl HostOutputStream for WritePipe { Ok(num) } - async fn writable(&self) -> Result<(), Error> { - Ok(()) + fn pollable(&self) -> HostPollable { + HostPollable::new(async {}) // FIXME } } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index d5d24b59b88c..8f7c3499b7a2 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -1,29 +1,90 @@ use crate::preview2::{ wasi::poll::poll::{self, Pollable}, - WasiView, + Table, TableError, WasiView, }; +use anyhow::{anyhow, Result}; use std::future::Future; use std::pin::Pin; +use std::task::{Context, Poll}; -pub struct HostPollable(Pin>>); +pub struct HostPollable(Pin + Send + Sync>>); + +impl HostPollable { + pub fn new(future: impl Future + Send + Sync + 'static) -> HostPollable { + HostPollable(Box::pin(future)) + } +} + +pub trait TablePollableExt { + fn push_host_pollable(&mut self, p: HostPollable) -> Result; + fn get_host_pollable_mut(&mut self, fd: u32) -> Result<&mut HostPollable, TableError>; + fn delete_host_pollable(&mut self, fd: u32) -> Result; +} + +impl TablePollableExt for Table { + fn push_host_pollable(&mut self, p: HostPollable) -> Result { + self.push(Box::new(p)) + } + fn get_host_pollable_mut(&mut self, fd: u32) -> Result<&mut HostPollable, TableError> { + self.get_mut::(fd) + } + fn delete_host_pollable(&mut self, fd: u32) -> Result { + self.delete::(fd) + } +} + +impl Future for HostPollable { + type Output = (); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + unsafe { self.map_unchecked_mut(|s| &mut s.0).poll(cx) } + } +} #[async_trait::async_trait] impl poll::Host for T { - async fn drop_pollable(&mut self, pollable: Pollable) -> anyhow::Result<()> { - self.table_mut().delete::(pollable)?; + async fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { + self.table_mut().delete_host_pollable(pollable)?; Ok(()) } - async fn poll_oneoff(&mut self, futures: Vec) -> anyhow::Result> { - // Convert `futures` into `Poll` subscriptions. - let len = futures.len(); - - // Convert the results into a list of `u8` to return. - let mut results = vec![0_u8; len]; - if todo!() { - let index: usize = todo!(); - results[index] = u8::from(true); + async fn poll_oneoff(&mut self, pollables: Vec) -> Result> { + struct PollOneoff<'a> { + table: &'a mut Table, + elems: &'a [Pollable], } - Ok(results) + impl<'a> Future for PollOneoff<'a> { + type Output = Result>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut any_ready = false; + let mut results = vec![false; self.elems.len()]; + for (ix, pollable) in self.elems.iter().enumerate() { + match self.table.get_host_pollable_mut(*pollable) { + Ok(f) => { + if let Poll::Ready(_) = Pin::new(f).poll(cx) { + results[ix] = true; + any_ready = true; + } + } + Err(e) => { + return Poll::Ready(Err( + anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}")) + )) + } + } + } + if any_ready { + Poll::Ready(Ok(results)) + } else { + Poll::Pending + } + } + } + + let bs = Pin::new(&mut PollOneoff { + table: self.table_mut(), + elems: &pollables, + }) + .await?; + Ok(bs.into_iter().map(|b| if b { 1 } else { 0 }).collect()) } } diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 01a7c342b68d..945eddc008b1 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -6,7 +6,7 @@ use crate::preview2::wasi::{ clocks::wall_clock::{self, Datetime}, poll::poll::Pollable, }; -use crate::preview2::{HostPollable, WasiView}; +use crate::preview2::{HostPollable, TablePollableExt, WasiView}; use cap_std::time::SystemTime; impl TryFrom for Datetime { @@ -53,7 +53,9 @@ impl monotonic_clock::Host for T { fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result { Ok(self .table_mut() - .push(todo!("FIXME integrate HostPollable here!"))?) + .push_host_pollable(HostPollable::new(tokio::time::sleep( + std::time::Duration::from_millis(1000), + )))?) } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 003db7a4b18f..d7f670114127 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -2,7 +2,7 @@ use crate::preview2::{ stream::{HostInputStream, HostOutputStream, TableStreamExt}, wasi::io::streams::{self, InputStream, OutputStream, StreamError}, wasi::poll::poll::Pollable, - TableError, WasiView, + HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -196,7 +196,7 @@ impl streams::Host for T { async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { Ok(self .table_mut() - .push(todo!("FIXME integrate HostPollable here"))?) + .push_host_pollable(HostPollable::new(async {}))?) // FIXME } async fn subscribe_to_output_stream( @@ -205,6 +205,6 @@ impl streams::Host for T { ) -> anyhow::Result { Ok(self .table_mut() - .push(todo!("FIXME integrate HostPollable here"))?) + .push_host_pollable(HostPollable::new(async {}))?) // FIXME } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 4214e38a6dbd..d372da56e1ae 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,7 +2,7 @@ use anyhow::Error; use std::convert::TryInto; use std::io::{self, Read, Write}; -use crate::preview2::{HostInputStream, HostOutputStream}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; pub struct Stdin(std::io::Stdin); @@ -41,8 +41,9 @@ impl HostInputStream for Stdin { Ok((num, num < nelem)) } - async fn readable(&self) -> Result<(), Error> { - Err(anyhow::anyhow!("idk")) + fn pollable(&self) -> HostPollable { + // Always ready immediately: + HostPollable::new(async {}) } } @@ -78,8 +79,9 @@ macro_rules! wasi_output_stream_impl { Ok(num) } - async fn writable(&self) -> Result<(), Error> { - Ok(()) + fn pollable(&self) -> HostPollable { + // Always ready immediately: + HostPollable::new(async {}) } } }; diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 0909e224fb84..eb7c465af212 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,4 +1,4 @@ -use crate::preview2::{Table, TableError}; +use crate::preview2::{HostPollable, Table, TableError}; use anyhow::Error; /// An input bytestream. @@ -48,8 +48,8 @@ pub trait HostInputStream: Send + Sync { Ok((nread, saw_end)) } - /// Test whether this stream is readable. - async fn readable(&self) -> Result<(), Error>; + /// Get the Pollable implementation for read readiness. + fn pollable(&self) -> HostPollable; } /// An output bytestream. @@ -117,8 +117,8 @@ pub trait HostOutputStream: Send + Sync { Ok(nwritten) } - /// Test whether this stream is writable. - async fn writable(&self) -> Result<(), Error>; + /// Get the Pollable implementation for write readiness. + fn pollable(&self) -> HostPollable; } pub trait TableStreamExt { From a3a3dbcfd71b1e13b05995f617e05d48fe51d114 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 8 Jun 2023 14:24:43 -0700 Subject: [PATCH 007/118] more fixes --- crates/wasi/src/preview2/filesystem.rs | 6 +++--- crates/wasi/src/preview2/pipe.rs | 17 +++-------------- crates/wasi/src/preview2/preview2/io.rs | 12 +++++------- crates/wasi/src/preview2/stdio.rs | 25 +++++++------------------ 4 files changed, 18 insertions(+), 42 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 5d9c67c3cbe6..26dc1c4c45c6 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -116,7 +116,7 @@ impl HostInputStream for FileInputStream { self.file.is_read_vectored_at() } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // FIXME + HostPollable::new(async {}) // Always immediately ready - file reads cannot block } } @@ -165,7 +165,7 @@ impl HostOutputStream for FileOutputStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // FIXME + HostPollable::new(async {}) // Always immediately ready - file writes cannot block } } @@ -201,6 +201,6 @@ impl HostOutputStream for FileAppendStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // FIXME + HostPollable::new(async {}) // Always immediately ready - file appends cannot block } } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 95f362019838..0688e19c431a 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -119,7 +119,8 @@ impl HostInputStream for ReadPipe { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // FIXME + HostPollable::new(async { todo!("pollable on a ReadPipe") }) + // FIXME } } @@ -195,18 +196,6 @@ impl HostOutputStream for WritePipe { let n = self.borrow().write(buf)?; Ok(n.try_into()?) } - - // TODO: Optimize for pipes. - /* - async fn splice( - &mut self, - src: &mut dyn InputStream, - nelem: u64, - ) -> Result { - todo!() - } - */ - async fn write_zeroes(&mut self, nelem: u64) -> Result { let num = io::copy( &mut io::Read::take(io::repeat(0), nelem), @@ -216,6 +205,6 @@ impl HostOutputStream for WritePipe { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // FIXME + HostPollable::new(async { todo!("pollable on a WritePipe") }) // FIXME } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index d7f670114127..cc009bef9f8f 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -2,7 +2,7 @@ use crate::preview2::{ stream::{HostInputStream, HostOutputStream, TableStreamExt}, wasi::io::streams::{self, InputStream, OutputStream, StreamError}, wasi::poll::poll::Pollable, - HostPollable, TableError, TablePollableExt, WasiView, + TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -194,17 +194,15 @@ impl streams::Host for T { } async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { - Ok(self - .table_mut() - .push_host_pollable(HostPollable::new(async {}))?) // FIXME + let pollable = self.table().get_input_stream(stream)?.pollable(); + Ok(self.table_mut().push_host_pollable(pollable)?) } async fn subscribe_to_output_stream( &mut self, stream: OutputStream, ) -> anyhow::Result { - Ok(self - .table_mut() - .push_host_pollable(HostPollable::new(async {}))?) // FIXME + let pollable = self.table().get_output_stream(stream)?.pollable(); + Ok(self.table_mut().push_host_pollable(pollable)?) } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index d372da56e1ae..8556c9488262 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -31,10 +31,11 @@ impl HostInputStream for Stdin { Err(err) => Err(err.into()), } } - #[cfg(can_vector)] - fn is_read_vectored(&self) { + /* this method can be implemented once `can_vector` stabilizes in std: + fn is_read_vectored(&self) -> bool { Read::is_read_vectored(&mut self.0) } + */ async fn skip(&mut self, nelem: u64) -> Result<(u64, bool), Error> { let num = io::copy(&mut io::Read::take(&mut self.0, nelem), &mut io::sink())?; @@ -42,8 +43,7 @@ impl HostInputStream for Stdin { } fn pollable(&self) -> HostPollable { - // Always ready immediately: - HostPollable::new(async {}) + HostPollable::new(async { todo!("pollable on stdin") }) } } @@ -59,29 +59,18 @@ macro_rules! wasi_output_stream_impl { let n = Write::write_vectored(&mut self.0, bufs)?; Ok(n.try_into()?) } - #[cfg(can_vector)] - fn is_write_vectored(&self) { + /* this method can be implemented once `can_vector` stablizes in std + fn is_write_vectored(&self) -> bool { Write::is_write_vectored(&mut self.0) } - // TODO: Optimize for stdio streams. - /* - async fn splice( - &mut self, - src: &mut dyn InputStream, - nelem: u64, - ) -> Result { - todo!() - } */ - async fn write_zeroes(&mut self, nelem: u64) -> Result { let num = io::copy(&mut io::Read::take(io::repeat(0), nelem), &mut self.0)?; Ok(num) } fn pollable(&self) -> HostPollable { - // Always ready immediately: - HostPollable::new(async {}) + HostPollable::new(async { todo!("pollable on stdio, stderr writes") }) } } }; From 1183313413c7233794dfa38d4695309e6e4b01b4 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 8 Jun 2023 16:28:41 -0700 Subject: [PATCH 008/118] pollable can trap, and implement clock properly --- crates/wasi/src/preview2/filesystem.rs | 6 ++--- crates/wasi/src/preview2/poll.rs | 20 +++++++++----- crates/wasi/src/preview2/preview2/clocks.rs | 29 +++++++++++++++++---- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 26dc1c4c45c6..352ea4b2a1bd 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -116,7 +116,7 @@ impl HostInputStream for FileInputStream { self.file.is_read_vectored_at() } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // Always immediately ready - file reads cannot block + HostPollable::new(async { Ok(()) }) // Always immediately ready - file reads cannot block } } @@ -165,7 +165,7 @@ impl HostOutputStream for FileOutputStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // Always immediately ready - file writes cannot block + HostPollable::new(async { Ok(()) }) // Always immediately ready - file writes cannot block } } @@ -201,6 +201,6 @@ impl HostOutputStream for FileAppendStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async {}) // Always immediately ready - file appends cannot block + HostPollable::new(async { Ok(()) }) // Always immediately ready - file appends cannot block } } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 8f7c3499b7a2..211d7751b53e 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -7,10 +7,10 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -pub struct HostPollable(Pin + Send + Sync>>); +pub struct HostPollable(Pin> + Send + Sync>>); impl HostPollable { - pub fn new(future: impl Future + Send + Sync + 'static) -> HostPollable { + pub fn new(future: impl Future> + Send + Sync + 'static) -> HostPollable { HostPollable(Box::pin(future)) } } @@ -34,8 +34,8 @@ impl TablePollableExt for Table { } impl Future for HostPollable { - type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + type Output = Result<()>; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { unsafe { self.map_unchecked_mut(|s| &mut s.0).poll(cx) } } } @@ -59,12 +59,18 @@ impl poll::Host for T { let mut results = vec![false; self.elems.len()]; for (ix, pollable) in self.elems.iter().enumerate() { match self.table.get_host_pollable_mut(*pollable) { - Ok(f) => { - if let Poll::Ready(_) = Pin::new(f).poll(cx) { + Ok(f) => match Pin::new(f).poll(cx) { + Poll::Ready(Ok(())) => { results[ix] = true; any_ready = true; } - } + Poll::Ready(Err(e)) => { + return Poll::Ready(Err( + e.context(format!("poll_oneoff[{ix}]: {pollable}")) + )); + } + Poll::Pending => {} + }, Err(e) => { return Poll::Ready(Err( anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}")) diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 945eddc008b1..324b9260812c 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -51,11 +51,30 @@ impl monotonic_clock::Host for T { } fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result { - Ok(self - .table_mut() - .push_host_pollable(HostPollable::new(tokio::time::sleep( - std::time::Duration::from_millis(1000), - )))?) + use std::time::Duration; + // Calculate time relative to clock object, which may not have the same zero + // point as tokio Inst::now() + let clock_now = self.ctx().clocks.monotonic.now(); + if absolute && when < clock_now { + // Deadline is in the past, so pollable is always ready: + Ok(self + .table_mut() + .push_host_pollable(HostPollable::new(async { Ok(()) }))?) + } else { + let duration = if absolute { + Duration::from_micros(clock_now - when) + } else { + Duration::from_micros(when) + }; + let deadline = tokio::time::Instant::now() + .checked_add(duration) + .ok_or_else(|| anyhow::anyhow!("time overflow: duration {duration:?}"))?; + Ok(self + .table_mut() + .push_host_pollable(HostPollable::new(async move { + Ok(tokio::time::sleep_until(deadline).await) + }))?) + } } } From 93913f74be0cfaabc17c25ceed161de29241ab59 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 9 Jun 2023 09:37:12 -0700 Subject: [PATCH 009/118] HostPollable is now a generator of futures because we need to be able to poll a pollable many times --- crates/wasi/src/preview2/filesystem.rs | 6 +-- crates/wasi/src/preview2/pipe.rs | 5 ++- crates/wasi/src/preview2/poll.rs | 45 +++++++++++---------- crates/wasi/src/preview2/preview2/clocks.rs | 6 +-- crates/wasi/src/preview2/stdio.rs | 4 +- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 352ea4b2a1bd..b0e413af5e7b 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -116,7 +116,7 @@ impl HostInputStream for FileInputStream { self.file.is_read_vectored_at() } fn pollable(&self) -> HostPollable { - HostPollable::new(async { Ok(()) }) // Always immediately ready - file reads cannot block + HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file reads cannot block } } @@ -165,7 +165,7 @@ impl HostOutputStream for FileOutputStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { Ok(()) }) // Always immediately ready - file writes cannot block + HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file writes cannot block } } @@ -201,6 +201,6 @@ impl HostOutputStream for FileAppendStream { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { Ok(()) }) // Always immediately ready - file appends cannot block + HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file appends cannot block } } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 0688e19c431a..faf811c31c18 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -119,7 +119,7 @@ impl HostInputStream for ReadPipe { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { todo!("pollable on a ReadPipe") }) + HostPollable::new(|| Box::pin(async { todo!("pollable on a ReadPipe") })) // FIXME } } @@ -205,6 +205,7 @@ impl HostOutputStream for WritePipe { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { todo!("pollable on a WritePipe") }) // FIXME + HostPollable::new(|| Box::pin(async { todo!("pollable on a WritePipe") })) + // FIXME } } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 211d7751b53e..a937229954e6 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -7,11 +7,18 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -pub struct HostPollable(Pin> + Send + Sync>>); +pub struct HostPollable( + Box Pin> + Send + Sync>> + Send + Sync>, +); impl HostPollable { - pub fn new(future: impl Future> + Send + Sync + 'static) -> HostPollable { - HostPollable(Box::pin(future)) + pub fn new( + mkfuture: impl Fn() -> Pin> + Send + Sync + 'static>> + + Send + + Sync + + 'static, + ) -> HostPollable { + HostPollable(Box::new(mkfuture)) } } @@ -33,13 +40,6 @@ impl TablePollableExt for Table { } } -impl Future for HostPollable { - type Output = Result<()>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - unsafe { self.map_unchecked_mut(|s| &mut s.0).poll(cx) } - } -} - #[async_trait::async_trait] impl poll::Host for T { async fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { @@ -59,18 +59,21 @@ impl poll::Host for T { let mut results = vec![false; self.elems.len()]; for (ix, pollable) in self.elems.iter().enumerate() { match self.table.get_host_pollable_mut(*pollable) { - Ok(f) => match Pin::new(f).poll(cx) { - Poll::Ready(Ok(())) => { - results[ix] = true; - any_ready = true; + Ok(mkf) => { + let mut f = mkf.0(); + match Pin::new(&mut f).poll(cx) { + Poll::Ready(Ok(())) => { + results[ix] = true; + any_ready = true; + } + Poll::Ready(Err(e)) => { + return Poll::Ready(Err( + e.context(format!("poll_oneoff[{ix}]: {pollable}")) + )); + } + Poll::Pending => {} } - Poll::Ready(Err(e)) => { - return Poll::Ready(Err( - e.context(format!("poll_oneoff[{ix}]: {pollable}")) - )); - } - Poll::Pending => {} - }, + } Err(e) => { return Poll::Ready(Err( anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}")) diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 324b9260812c..c6dedb2f0cb1 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -59,7 +59,7 @@ impl monotonic_clock::Host for T { // Deadline is in the past, so pollable is always ready: Ok(self .table_mut() - .push_host_pollable(HostPollable::new(async { Ok(()) }))?) + .push_host_pollable(HostPollable::new(|| Box::pin(async { Ok(()) })))?) } else { let duration = if absolute { Duration::from_micros(clock_now - when) @@ -71,8 +71,8 @@ impl monotonic_clock::Host for T { .ok_or_else(|| anyhow::anyhow!("time overflow: duration {duration:?}"))?; Ok(self .table_mut() - .push_host_pollable(HostPollable::new(async move { - Ok(tokio::time::sleep_until(deadline).await) + .push_host_pollable(HostPollable::new(move || { + Box::pin(async move { Ok(tokio::time::sleep_until(deadline).await) }) }))?) } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 8556c9488262..11d6312dc2ee 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -43,7 +43,7 @@ impl HostInputStream for Stdin { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { todo!("pollable on stdin") }) + HostPollable::new(|| Box::pin(async { todo!("pollable on stdin") })) } } @@ -70,7 +70,7 @@ macro_rules! wasi_output_stream_impl { } fn pollable(&self) -> HostPollable { - HostPollable::new(async { todo!("pollable on stdio, stderr writes") }) + HostPollable::new(|| Box::pin(async { todo!("pollable on stdio, stderr writes") })) } } }; From f670a06b25ecddf6a977a63901cec6dcff653cab Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 8 Jun 2023 16:45:04 -0700 Subject: [PATCH 010/118] explain various todo!s --- crates/wasi/src/preview2/preview1/mod.rs | 10 +++++----- crates/wasi/src/preview2/preview2/clocks.rs | 6 +++--- crates/wasi/src/preview2/preview2/filesystem.rs | 16 ++++++++-------- crates/wasi/src/preview2/preview2/io.rs | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index cba244044410..7a9af6164af8 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1731,7 +1731,7 @@ impl< events: &GuestPtr<'a, types::Event>, nsubscriptions: types::Size, ) -> Result { - todo!() + todo!("preview1 poll_oneoff is not implemented") } #[instrument(skip(self))] @@ -1778,7 +1778,7 @@ impl< fd: types::Fd, flags: types::Fdflags, ) -> Result { - todo!() + todo!("preview1 sock_accept is not implemented") } #[allow(unused_variables)] @@ -1789,7 +1789,7 @@ impl< ri_data: &types::IovecArray<'a>, ri_flags: types::Riflags, ) -> Result<(types::Size, types::Roflags), types::Error> { - todo!() + todo!("preview1 sock_recv is not implemented") } #[allow(unused_variables)] @@ -1800,12 +1800,12 @@ impl< si_data: &types::CiovecArray<'a>, _si_flags: types::Siflags, ) -> Result { - todo!() + todo!("preview1 sock_send is not implemented") } #[allow(unused_variables)] #[instrument(skip(self))] fn sock_shutdown(&mut self, fd: types::Fd, how: types::Sdflags) -> Result<(), types::Error> { - todo!() + todo!("preview1 sock_shutdown is not implemented") } } diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index c6dedb2f0cb1..70ebb05727d0 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -80,14 +80,14 @@ impl monotonic_clock::Host for T { impl timezone::Host for T { fn display(&mut self, timezone: Timezone, when: Datetime) -> anyhow::Result { - todo!() + todo!("timezone display is not implemented") } fn utc_offset(&mut self, timezone: Timezone, when: Datetime) -> anyhow::Result { - todo!() + todo!("timezone utc_offset is not implemented") } fn drop_timezone(&mut self, timezone: Timezone) -> anyhow::Result<()> { - todo!() + todo!("timezone drop is not implemented") } } diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 4b3b84ea892b..5483de2d00b2 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -640,7 +640,7 @@ impl filesystem::Host for T { _path: String, _access: filesystem::AccessType, ) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem access_at is not implemented") } fn change_file_permissions_at( @@ -650,7 +650,7 @@ impl filesystem::Host for T { _path: String, _mode: filesystem::Modes, ) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem change_file_permissions_at is not implemented") } fn change_directory_permissions_at( @@ -660,27 +660,27 @@ impl filesystem::Host for T { _path: String, _mode: filesystem::Modes, ) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem change_directory_permissions_at is not implemented") } fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem lock_shared is not implemented") } fn lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem lock_exclusive is not implemented") } fn try_lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem try_lock_shared is not implemented") } fn try_lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem try_lock_exclusive is not implemented") } fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!() + todo!("filesystem unlock is not implemented") } fn read_via_stream( diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index cc009bef9f8f..c40b53b9daa4 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -151,7 +151,7 @@ impl streams::Host for T { Ok(bytes_spliced) */ - todo!() + todo!("stream splice is not implemented") } async fn blocking_splice( @@ -190,7 +190,7 @@ impl streams::Host for T { Ok(bytes_spliced) */ - todo!() + todo!("stream forward is not implemented") } async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { From a71719bd0b2c4a69c10db5c018bb14f2ee3b1771 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 9 Jun 2023 15:05:21 -0700 Subject: [PATCH 011/118] Synchronous version of the wasi-preview2-components tests --- Cargo.lock | 1 + crates/test-programs/reactor-tests/Cargo.toml | 2 +- .../tests/wasi-preview2-components-sync.rs | 304 ++++++++++++++++++ crates/wasi/Cargo.toml | 1 + crates/wasi/src/preview2/pipe.rs | 7 + crates/wasi/src/preview2/poll.rs | 44 ++- crates/wasi/src/preview2/preview2/clocks.rs | 18 +- crates/wasi/src/preview2/preview2/io.rs | 128 ++++++++ crates/wasi/src/preview2/stdio.rs | 7 + crates/wasi/src/preview2/stream.rs | 5 + crates/wasi/src/preview2/wasi/command.rs | 48 +++ crates/wasi/src/preview2/wasi/mod.rs | 16 + 12 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 crates/test-programs/tests/wasi-preview2-components-sync.rs diff --git a/Cargo.lock b/Cargo.lock index a3faf149ea6f..7841aa248103 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4236,6 +4236,7 @@ dependencies = [ "fs-set-times", "io-extras", "libc", + "once_cell", "rustix", "system-interface", "thiserror", diff --git a/crates/test-programs/reactor-tests/Cargo.toml b/crates/test-programs/reactor-tests/Cargo.toml index df7752858342..28a645a9401e 100644 --- a/crates/test-programs/reactor-tests/Cargo.toml +++ b/crates/test-programs/reactor-tests/Cargo.toml @@ -8,4 +8,4 @@ publish = false crate-type=["cdylib"] [dependencies] -wit-bindgen = { workspace = true } +wit-bindgen = { workspace = true, features = ["macros", "realloc"] } diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs new file mode 100644 index 000000000000..d80e1e080f5c --- /dev/null +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -0,0 +1,304 @@ +#![cfg(feature = "test_programs")] +use anyhow::Result; +use tempfile::TempDir; +use wasmtime::{component::Linker, Config, Engine, Store}; +use wasmtime_wasi::preview2::{ + pipe::WritePipe, + wasi::command::sync::{add_to_linker, Command}, + DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, +}; + +lazy_static::lazy_static! { + static ref ENGINE: Engine = { + let mut config = Config::new(); + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.wasm_component_model(true); + config.async_support(false); + + let engine = Engine::new(&config).unwrap(); + engine + }; +} +// uses ENGINE, creates a fn get_component(&str) -> Component +include!(concat!(env!("OUT_DIR"), "/wasi_tests_components.rs")); + +pub fn prepare_workspace(exe_name: &str) -> Result { + let prefix = format!("wasi_components_{}_", exe_name); + let tempdir = tempfile::Builder::new().prefix(&prefix).tempdir()?; + Ok(tempdir) +} + +fn run(name: &str, inherit_stdio: bool) -> Result<()> { + let workspace = prepare_workspace(name)?; + let stdout = WritePipe::new_in_memory(); + let stderr = WritePipe::new_in_memory(); + let r = { + let mut linker = Linker::new(&ENGINE); + add_to_linker(&mut linker)?; + + // Create our wasi context. + // Additionally register any preopened directories if we have them. + let mut builder = WasiCtxBuilder::new(); + + if inherit_stdio { + builder = builder.inherit_stdio(); + } else { + builder = builder + .set_stdout(stdout.clone()) + .set_stderr(stderr.clone()); + } + builder = builder.set_args(&[name, "."]); + println!("preopen: {:?}", workspace); + let preopen_dir = + cap_std::fs::Dir::open_ambient_dir(workspace.path(), cap_std::ambient_authority())?; + builder = builder.push_preopened_dir(preopen_dir, DirPerms::all(), FilePerms::all(), "."); + for (var, val) in test_programs::wasi_tests_environment() { + builder = builder.push_env(var, val); + } + + let mut table = Table::new(); + let wasi = builder.build(&mut table)?; + struct Ctx { + wasi: WasiCtx, + table: Table, + } + impl WasiView for Ctx { + fn ctx(&self) -> &WasiCtx { + &self.wasi + } + fn ctx_mut(&mut self) -> &mut WasiCtx { + &mut self.wasi + } + fn table(&self) -> &Table { + &self.table + } + fn table_mut(&mut self) -> &mut Table { + &mut self.table + } + } + + let ctx = Ctx { wasi, table }; + let mut store = Store::new(&ENGINE, ctx); + let (command, _instance) = Command::instantiate(&mut store, &get_component(name), &linker)?; + command + .call_run(&mut store)? + .map_err(|()| anyhow::anyhow!("run returned a failure"))?; + Ok(()) + }; + + r.map_err(move |trap: anyhow::Error| { + let stdout = stdout + .try_into_inner() + .expect("sole ref to stdout") + .into_inner(); + if !stdout.is_empty() { + println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); + } + let stderr = stderr + .try_into_inner() + .expect("sole ref to stderr") + .into_inner(); + if !stderr.is_empty() { + println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); + } + trap.context(format!( + "error while testing wasi-tests {} with cap-std-sync", + name + )) + })?; + Ok(()) +} + +// Below here is mechanical: there should be one test for every binary in +// wasi-tests. The only differences should be should_panic annotations for +// tests which fail. +#[test_log::test] +fn big_random_buf() { + run("big_random_buf", true).unwrap() +} +#[test_log::test] +fn clock_time_get() { + run("clock_time_get", true).unwrap() +} +#[test_log::test] +fn close_preopen() { + run("close_preopen", true).unwrap() +} +#[test_log::test] +fn dangling_fd() { + run("dangling_fd", true).unwrap() +} +#[test_log::test] +fn dangling_symlink() { + run("dangling_symlink", true).unwrap() +} +#[test_log::test] +fn directory_seek() { + run("directory_seek", true).unwrap() +} +#[test_log::test] +fn dir_fd_op_failures() { + run("dir_fd_op_failures", true).unwrap() +} +#[test_log::test] +fn fd_advise() { + run("fd_advise", true).unwrap() +} +#[test_log::test] +fn fd_filestat_get() { + run("fd_filestat_get", true).unwrap() +} +#[test_log::test] +fn fd_filestat_set() { + run("fd_filestat_set", true).unwrap() +} +#[test_log::test] +fn fd_flags_set() { + run("fd_flags_set", true).unwrap() +} +#[test_log::test] +fn fd_readdir() { + run("fd_readdir", true).unwrap() +} +#[test_log::test] +fn file_allocate() { + run("file_allocate", true).unwrap() +} +#[test_log::test] +fn file_pread_pwrite() { + run("file_pread_pwrite", true).unwrap() +} +#[test_log::test] +fn file_seek_tell() { + run("file_seek_tell", true).unwrap() +} +#[test_log::test] +fn file_truncation() { + run("file_truncation", true).unwrap() +} +#[test_log::test] +fn file_unbuffered_write() { + run("file_unbuffered_write", true).unwrap() +} +#[test_log::test] +#[cfg_attr(windows, should_panic)] +fn interesting_paths() { + run("interesting_paths", true).unwrap() +} +#[test_log::test] +fn isatty() { + run("isatty", true).unwrap() +} +#[test_log::test] +fn nofollow_errors() { + run("nofollow_errors", true).unwrap() +} +#[test_log::test] +fn overwrite_preopen() { + run("overwrite_preopen", true).unwrap() +} +#[test_log::test] +fn path_exists() { + run("path_exists", true).unwrap() +} +#[test_log::test] +fn path_filestat() { + run("path_filestat", true).unwrap() +} +#[test_log::test] +fn path_link() { + run("path_link", true).unwrap() +} +#[test_log::test] +fn path_open_create_existing() { + run("path_open_create_existing", true).unwrap() +} +#[test_log::test] +fn path_open_read_write() { + run("path_open_read_write", true).unwrap() +} +#[test_log::test] +fn path_open_dirfd_not_dir() { + run("path_open_dirfd_not_dir", true).unwrap() +} +#[test_log::test] +fn path_open_missing() { + run("path_open_missing", true).unwrap() +} +#[test_log::test] +fn path_open_nonblock() { + run("path_open_nonblock", true).unwrap() +} +#[test_log::test] +fn path_rename_dir_trailing_slashes() { + run("path_rename_dir_trailing_slashes", true).unwrap() +} +#[test_log::test] +#[should_panic] +fn path_rename_file_trailing_slashes() { + run("path_rename_file_trailing_slashes", false).unwrap() +} +#[test_log::test] +fn path_rename() { + run("path_rename", true).unwrap() +} +#[test_log::test] +fn path_symlink_trailing_slashes() { + run("path_symlink_trailing_slashes", true).unwrap() +} +#[test_log::test] +#[cfg_attr(windows, should_panic)] +fn poll_oneoff_files() { + run("poll_oneoff_files", false).unwrap() +} +#[test_log::test] +// This is a known bug with the preview 2 implementation: +#[should_panic] +fn poll_oneoff_stdio() { + run("poll_oneoff_stdio", true).unwrap() +} +#[test_log::test] +fn readlink() { + run("readlink", true).unwrap() +} +#[test_log::test] +#[should_panic] +fn remove_directory_trailing_slashes() { + run("remove_directory_trailing_slashes", false).unwrap() +} +#[test_log::test] +fn remove_nonempty_directory() { + run("remove_nonempty_directory", true).unwrap() +} +#[test_log::test] +fn renumber() { + run("renumber", true).unwrap() +} +#[test_log::test] +fn sched_yield() { + run("sched_yield", true).unwrap() +} +#[test_log::test] +fn stdio() { + run("stdio", true).unwrap() +} +#[test_log::test] +fn symlink_create() { + run("symlink_create", true).unwrap() +} +#[test_log::test] +fn symlink_filestat() { + run("symlink_filestat", true).unwrap() +} +#[test_log::test] +fn symlink_loop() { + run("symlink_loop", true).unwrap() +} +#[test_log::test] +fn unlink_file_trailing_slashes() { + run("unlink_file_trailing_slashes", true).unwrap() +} +#[test_log::test] +fn path_open_preopen() { + run("path_open_preopen", true).unwrap() +} diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index d4cece5e3cad..d3e92227ed45 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -20,6 +20,7 @@ wasi-cap-std-sync = { workspace = true, optional = true } wasi-tokio = { workspace = true, optional = true } wiggle = { workspace = true, optional = true } libc = { workspace = true } +once_cell = { workspace = true } tokio = { workspace = true, optional = true, features = ["time"] } thiserror = { workspace = true, optional = true } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index faf811c31c18..b7085bf43c7b 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -119,6 +119,9 @@ impl HostInputStream for ReadPipe { } fn pollable(&self) -> HostPollable { + // TODO(elliottt): this should probably be: + // 1. take the lock + // 2. check if there's anything inside of it to be read HostPollable::new(|| Box::pin(async { todo!("pollable on a ReadPipe") })) // FIXME } @@ -191,6 +194,7 @@ impl WritePipe>> { } #[async_trait::async_trait] +// TODO: can we remove the `Any` constraint here? impl HostOutputStream for WritePipe { async fn write(&mut self, buf: &[u8]) -> Result { let n = self.borrow().write(buf)?; @@ -205,6 +209,9 @@ impl HostOutputStream for WritePipe { } fn pollable(&self) -> HostPollable { + // TODO(elliottt): because this is using only a `Write` constraint, and that trait doesn't + // provide a way to check if there's space available, this should trivially return that + // there's space to write. HostPollable::new(|| Box::pin(async { todo!("pollable on a WritePipe") })) // FIXME } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index a937229954e6..3b0410aff118 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -55,9 +55,14 @@ impl poll::Host for T { impl<'a> Future for PollOneoff<'a> { type Output = Result>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + + // TODO(elliottt): why do we never re-enter the poll function? + let mut any_ready = false; let mut results = vec![false; self.elems.len()]; for (ix, pollable) in self.elems.iter().enumerate() { + tracing::trace!("polling!"); + match self.table.get_host_pollable_mut(*pollable) { Ok(mkf) => { let mut f = mkf.0(); @@ -71,7 +76,9 @@ impl poll::Host for T { e.context(format!("poll_oneoff[{ix}]: {pollable}")) )); } - Poll::Pending => {} + Poll::Pending => { + tracing::trace!("pending!"); + } } } Err(e) => { @@ -84,6 +91,7 @@ impl poll::Host for T { if any_ready { Poll::Ready(Ok(results)) } else { + tracing::trace!("overall, we're pending!"); Poll::Pending } } @@ -93,7 +101,41 @@ impl poll::Host for T { table: self.table_mut(), elems: &pollables, }) + // TODO: why does poll only get called once? .await?; Ok(bs.into_iter().map(|b| if b { 1 } else { 0 }).collect()) } } + +pub mod sync { + use crate::preview2::{ + wasi::poll::poll::Host as AsyncHost, + wasi::sync_io::poll::poll::{self, Pollable}, + WasiView, + }; + use anyhow::Result; + use tokio::runtime::{Builder, Handle, Runtime}; + + pub fn with_tokio() -> Handle { + match Handle::try_current() { + Ok(h) => h, + Err(_) => { + use once_cell::sync::Lazy; + static RUNTIME: Lazy = + Lazy::new(|| Builder::new_current_thread().enable_time().build().unwrap()); + let _enter = RUNTIME.enter(); + RUNTIME.handle().clone() + } + } + } + + impl poll::Host for T { + fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { + with_tokio().block_on(async { AsyncHost::drop_pollable(self, pollable).await }) + } + + fn poll_oneoff(&mut self, pollables: Vec) -> Result> { + with_tokio().block_on(async { AsyncHost::poll_oneoff(self, pollables).await }) + } + } +} diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 70ebb05727d0..4816814ddaea 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -62,17 +62,29 @@ impl monotonic_clock::Host for T { .push_host_pollable(HostPollable::new(|| Box::pin(async { Ok(()) })))?) } else { let duration = if absolute { - Duration::from_micros(clock_now - when) + Duration::from_nanos(clock_now - when) } else { - Duration::from_micros(when) + Duration::from_nanos(when) }; let deadline = tokio::time::Instant::now() .checked_add(duration) .ok_or_else(|| anyhow::anyhow!("time overflow: duration {duration:?}"))?; + tracing::trace!( + "deadline = {:?}, now = {:?}", + deadline, + tokio::time::Instant::now() + ); Ok(self .table_mut() .push_host_pollable(HostPollable::new(move || { - Box::pin(async move { Ok(tokio::time::sleep_until(deadline).await) }) + Box::pin(async move { + tracing::trace!( + "mkf: deadline = {:?}, now = {:?}", + deadline, + tokio::time::Instant::now() + ); + Ok(tokio::time::sleep_until(deadline).await) + }) }))?) } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index c40b53b9daa4..4eab235e8d25 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -206,3 +206,131 @@ impl streams::Host for T { Ok(self.table_mut().push_host_pollable(pollable)?) } } + +pub mod sync { + use crate::preview2::{ + poll::sync::with_tokio, + stream::{HostInputStream, HostOutputStream, TableStreamExt}, + wasi::io::streams::{Host as AsyncHost, StreamError as AsyncStreamError}, + wasi::sync_io::io::streams::{self, InputStream, OutputStream, StreamError}, + wasi::sync_io::poll::poll::Pollable, + TableError, TablePollableExt, WasiView, + }; + use anyhow::anyhow; + + impl streams::Host for T { + fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { + with_tokio().block_on(async { AsyncHost::drop_input_stream(self, stream).await }) + } + + fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { + with_tokio().block_on(async { AsyncHost::drop_output_stream(self, stream).await }) + } + + fn read( + &mut self, + stream: InputStream, + len: u64, + ) -> Result<(Vec, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::read(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn blocking_read( + &mut self, + stream: InputStream, + len: u64, + ) -> Result<(Vec, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::blocking_read(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { + with_tokio() + .block_on(async { AsyncHost::write(self, stream, bytes).await }) + .map_err(streams::Error::from) + } + + fn blocking_write( + &mut self, + stream: OutputStream, + bytes: Vec, + ) -> Result { + with_tokio() + .block_on(async { AsyncHost::write(self, stream, bytes).await }) + .map_err(streams::Error::from) + } + + fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::skip(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn blocking_skip( + &mut self, + stream: InputStream, + len: u64, + ) -> Result<(u64, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn write_zeroes(&mut self, stream: OutputStream, len: u64) -> Result { + with_tokio() + .block_on(async { AsyncHost::write_zeroes(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn blocking_write_zeroes( + &mut self, + stream: OutputStream, + len: u64, + ) -> Result { + with_tokio() + .block_on(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) + .map_err(streams::Error::from) + } + + fn splice( + &mut self, + src: InputStream, + dst: OutputStream, + len: u64, + ) -> Result<(u64, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::splice(self, src, dst, len).await }) + .map_err(streams::Error::from) + } + + fn blocking_splice( + &mut self, + src: InputStream, + dst: OutputStream, + len: u64, + ) -> Result<(u64, bool), streams::Error> { + with_tokio() + .block_on(async { AsyncHost::blocking_splice(self, src, dst, len).await }) + .map_err(streams::Error::from) + } + + fn forward(&mut self, src: InputStream, dst: OutputStream) -> Result { + with_tokio() + .block_on(async { AsyncHost::forward(self, src, dst).await }) + .map_err(streams::Error::from) + } + + fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { + with_tokio() + .block_on(async { AsyncHost::subscribe_to_input_stream(self, stream).await }) + } + + fn subscribe_to_output_stream(&mut self, stream: OutputStream) -> anyhow::Result { + with_tokio() + .block_on(async { AsyncHost::subscribe_to_output_stream(self, stream).await }) + } + } +} diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 11d6312dc2ee..52af008ce388 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -43,6 +43,10 @@ impl HostInputStream for Stdin { } fn pollable(&self) -> HostPollable { + // TODO(elliottt): this can be a read with an empty buffer to check for ready, but on + // windows there is a special function that needs to be called in a worker thread, as stdin + // is special. There is already code in wasi-common for creating the worker thread, copy + // that. HostPollable::new(|| Box::pin(async { todo!("pollable on stdin") })) } } @@ -70,6 +74,9 @@ macro_rules! wasi_output_stream_impl { } fn pollable(&self) -> HostPollable { + // TODO(elliottt): not clear how to implement this, but writing an empty buffer is + // probably the right next step. It's not clear how stdout/stderr could not be + // ready for writing. HostPollable::new(|| Box::pin(async { todo!("pollable on stdio, stderr writes") })) } } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index eb7c465af212..a23dbe058e85 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,6 +1,11 @@ use crate::preview2::{HostPollable, Table, TableError}; use anyhow::Error; +// TODO(elliottt): wrapper of (Box, buffering), then impl HostInputStream +// for that type, then do the same for a dyn AsyncWrite with buffering for HostOutputStream. +// Testing could be to send the guest a timestamp and have them check that it's 100ms in the past? +// Try putting this in the command tests. + /// An input bytestream. /// /// This is "pseudo" because the real streams will be a type in wit, and diff --git a/crates/wasi/src/preview2/wasi/command.rs b/crates/wasi/src/preview2/wasi/command.rs index 9dc472536ca3..37b364969a48 100644 --- a/crates/wasi/src/preview2/wasi/command.rs +++ b/crates/wasi/src/preview2/wasi/command.rs @@ -41,3 +41,51 @@ pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> any crate::preview2::wasi::cli_base::stderr::add_to_linker(l, |t| t)?; Ok(()) } + +pub mod sync { + use crate::preview2::WasiView; + + wasmtime::component::bindgen!({ + world: "wasi:preview/command", + tracing: true, + async: false, + trappable_error_type: { + "filesystem"::"error-code": Error, + "streams"::"stream-error": Error, + }, + with: { + "wasi:filesystem/filesystem": crate::preview2::wasi::filesystem::filesystem, + "wasi:clocks/monotonic_clock": crate::preview2::wasi::clocks::monotonic_clock, + "wasi:poll/poll": crate::preview2::wasi::sync_io::poll::poll, + "wasi:io/streams": crate::preview2::wasi::sync_io::io::streams, + "wasi:clocks/timezone": crate::preview2::wasi::clocks::timezone, + "wasi:clocks/wall_clock": crate::preview2::wasi::clocks::wall_clock, + "wasi:random/random": crate::preview2::wasi::random::random, + "wasi:cli_base/environment": crate::preview2::wasi::cli_base::environment, + "wasi:cli_base/exit": crate::preview2::wasi::cli_base::exit, + "wasi:cli_base/preopens": crate::preview2::wasi::cli_base::preopens, + "wasi:cli_base/stdin": crate::preview2::wasi::cli_base::stdin, + "wasi:cli_base/stdout": crate::preview2::wasi::cli_base::stdout, + "wasi:cli_base/stderr": crate::preview2::wasi::cli_base::stderr, + }, + }); + + pub fn add_to_linker( + l: &mut wasmtime::component::Linker, + ) -> anyhow::Result<()> { + crate::preview2::wasi::clocks::wall_clock::add_to_linker(l, |t| t)?; + crate::preview2::wasi::clocks::monotonic_clock::add_to_linker(l, |t| t)?; + crate::preview2::wasi::clocks::timezone::add_to_linker(l, |t| t)?; + crate::preview2::wasi::filesystem::filesystem::add_to_linker(l, |t| t)?; + crate::preview2::wasi::sync_io::poll::poll::add_to_linker(l, |t| t)?; + crate::preview2::wasi::sync_io::io::streams::add_to_linker(l, |t| t)?; + crate::preview2::wasi::random::random::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::exit::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::environment::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::preopens::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::stdin::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::stdout::add_to_linker(l, |t| t)?; + crate::preview2::wasi::cli_base::stderr::add_to_linker(l, |t| t)?; + Ok(()) + } +} diff --git a/crates/wasi/src/preview2/wasi/mod.rs b/crates/wasi/src/preview2/wasi/mod.rs index 5c8599421a6a..63f99c6e2f18 100644 --- a/crates/wasi/src/preview2/wasi/mod.rs +++ b/crates/wasi/src/preview2/wasi/mod.rs @@ -15,6 +15,22 @@ pub mod sync_io { }); } pub use self::_internal::wasi::{io, poll}; + + impl From for io::streams::StreamError { + fn from(other: super::io::streams::StreamError) -> Self { + // There are no cases for this record. + Self {} + } + } + + impl From for io::streams::Error { + fn from(other: super::io::streams::Error) -> Self { + match other.downcast() { + Ok(se) => io::streams::Error::from(io::streams::StreamError::from(se)), + Err(e) => io::streams::Error::trap(e), + } + } + } } pub(crate) mod _internal_io { From 4d878595c007b2a22c6d46eadd2310367264334d Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 15 Jun 2023 11:47:54 -0700 Subject: [PATCH 012/118] Change with_tokio to accept the future as an argument --- crates/wasi/src/preview2/poll.rs | 11 +++--- crates/wasi/src/preview2/preview2/io.rs | 45 +++++++++---------------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 3b0410aff118..3d999b08ef23 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -108,6 +108,7 @@ impl poll::Host for T { } pub mod sync { + use std::future::Future; use crate::preview2::{ wasi::poll::poll::Host as AsyncHost, wasi::sync_io::poll::poll::{self, Pollable}, @@ -116,26 +117,26 @@ pub mod sync { use anyhow::Result; use tokio::runtime::{Builder, Handle, Runtime}; - pub fn with_tokio() -> Handle { + pub fn block_on(f: F) -> F::Output { match Handle::try_current() { - Ok(h) => h, + Ok(h) => h.block_on(f), Err(_) => { use once_cell::sync::Lazy; static RUNTIME: Lazy = Lazy::new(|| Builder::new_current_thread().enable_time().build().unwrap()); let _enter = RUNTIME.enter(); - RUNTIME.handle().clone() + RUNTIME.block_on(f) } } } impl poll::Host for T { fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { - with_tokio().block_on(async { AsyncHost::drop_pollable(self, pollable).await }) + block_on(async { AsyncHost::drop_pollable(self, pollable).await }) } fn poll_oneoff(&mut self, pollables: Vec) -> Result> { - with_tokio().block_on(async { AsyncHost::poll_oneoff(self, pollables).await }) + block_on(async { AsyncHost::poll_oneoff(self, pollables).await }) } } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 4eab235e8d25..da994cfd03dd 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -209,7 +209,7 @@ impl streams::Host for T { pub mod sync { use crate::preview2::{ - poll::sync::with_tokio, + poll::sync::block_on, stream::{HostInputStream, HostOutputStream, TableStreamExt}, wasi::io::streams::{Host as AsyncHost, StreamError as AsyncStreamError}, wasi::sync_io::io::streams::{self, InputStream, OutputStream, StreamError}, @@ -220,11 +220,11 @@ pub mod sync { impl streams::Host for T { fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { - with_tokio().block_on(async { AsyncHost::drop_input_stream(self, stream).await }) + block_on(async { AsyncHost::drop_input_stream(self, stream).await }) } fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { - with_tokio().block_on(async { AsyncHost::drop_output_stream(self, stream).await }) + block_on(async { AsyncHost::drop_output_stream(self, stream).await }) } fn read( @@ -232,8 +232,7 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(Vec, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::read(self, stream, len).await }) + block_on(async { AsyncHost::read(self, stream, len).await }) .map_err(streams::Error::from) } @@ -242,14 +241,12 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(Vec, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::blocking_read(self, stream, len).await }) + block_on(async { AsyncHost::blocking_read(self, stream, len).await }) .map_err(streams::Error::from) } fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { - with_tokio() - .block_on(async { AsyncHost::write(self, stream, bytes).await }) + block_on(async { AsyncHost::write(self, stream, bytes).await }) .map_err(streams::Error::from) } @@ -258,14 +255,12 @@ pub mod sync { stream: OutputStream, bytes: Vec, ) -> Result { - with_tokio() - .block_on(async { AsyncHost::write(self, stream, bytes).await }) + block_on(async { AsyncHost::write(self, stream, bytes).await }) .map_err(streams::Error::from) } fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::skip(self, stream, len).await }) + block_on(async { AsyncHost::skip(self, stream, len).await }) .map_err(streams::Error::from) } @@ -274,14 +269,12 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(u64, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) + block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) .map_err(streams::Error::from) } fn write_zeroes(&mut self, stream: OutputStream, len: u64) -> Result { - with_tokio() - .block_on(async { AsyncHost::write_zeroes(self, stream, len).await }) + block_on(async { AsyncHost::write_zeroes(self, stream, len).await }) .map_err(streams::Error::from) } @@ -290,8 +283,7 @@ pub mod sync { stream: OutputStream, len: u64, ) -> Result { - with_tokio() - .block_on(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) + block_on(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) .map_err(streams::Error::from) } @@ -301,8 +293,7 @@ pub mod sync { dst: OutputStream, len: u64, ) -> Result<(u64, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::splice(self, src, dst, len).await }) + block_on(async { AsyncHost::splice(self, src, dst, len).await }) .map_err(streams::Error::from) } @@ -312,25 +303,21 @@ pub mod sync { dst: OutputStream, len: u64, ) -> Result<(u64, bool), streams::Error> { - with_tokio() - .block_on(async { AsyncHost::blocking_splice(self, src, dst, len).await }) + block_on(async { AsyncHost::blocking_splice(self, src, dst, len).await }) .map_err(streams::Error::from) } fn forward(&mut self, src: InputStream, dst: OutputStream) -> Result { - with_tokio() - .block_on(async { AsyncHost::forward(self, src, dst).await }) + block_on(async { AsyncHost::forward(self, src, dst).await }) .map_err(streams::Error::from) } fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { - with_tokio() - .block_on(async { AsyncHost::subscribe_to_input_stream(self, stream).await }) + block_on(async { AsyncHost::subscribe_to_input_stream(self, stream).await }) } fn subscribe_to_output_stream(&mut self, stream: OutputStream) -> anyhow::Result { - with_tokio() - .block_on(async { AsyncHost::subscribe_to_output_stream(self, stream).await }) + block_on(async { AsyncHost::subscribe_to_output_stream(self, stream).await }) } } } From 6c4f0ae09356a5a86cc75ab58516730995140a91 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 15 Jun 2023 13:04:38 -0700 Subject: [PATCH 013/118] Store futures in the PollOneoff struct instead, to avoid dropping them --- crates/wasi/src/preview2/poll.rs | 86 +++++++++++++------------------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 3d999b08ef23..ce903f260fd7 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -7,17 +7,12 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -pub struct HostPollable( - Box Pin> + Send + Sync>> + Send + Sync>, -); +type ReadynessFuture = Pin> + Send + Sync>>; + +pub struct HostPollable(Box ReadynessFuture + Send + Sync>); impl HostPollable { - pub fn new( - mkfuture: impl Fn() -> Pin> + Send + Sync + 'static>> - + Send - + Sync - + 'static, - ) -> HostPollable { + pub fn new(mkfuture: impl Fn() -> ReadynessFuture + Send + Sync + 'static) -> HostPollable { HostPollable(Box::new(mkfuture)) } } @@ -48,73 +43,64 @@ impl poll::Host for T { } async fn poll_oneoff(&mut self, pollables: Vec) -> Result> { - struct PollOneoff<'a> { - table: &'a mut Table, - elems: &'a [Pollable], + struct PollOneoff { + elems: Vec<(u32, ReadynessFuture)>, } - impl<'a> Future for PollOneoff<'a> { - type Output = Result>; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - - // TODO(elliottt): why do we never re-enter the poll function? + impl Future for PollOneoff { + type Output = Result>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut any_ready = false; - let mut results = vec![false; self.elems.len()]; - for (ix, pollable) in self.elems.iter().enumerate() { - tracing::trace!("polling!"); - - match self.table.get_host_pollable_mut(*pollable) { - Ok(mkf) => { - let mut f = mkf.0(); - match Pin::new(&mut f).poll(cx) { - Poll::Ready(Ok(())) => { - results[ix] = true; - any_ready = true; - } - Poll::Ready(Err(e)) => { - return Poll::Ready(Err( - e.context(format!("poll_oneoff[{ix}]: {pollable}")) - )); - } - Poll::Pending => { - tracing::trace!("pending!"); - } - } + let mut results = vec![0; self.elems.len()]; + for (ix, (pollable, f)) in self.elems.iter_mut().enumerate() { + // NOTE: we don't need to guard against polling any of the elems more than + // once, as a single one becoming ready will cause the whole PollOneoff future + // to become ready. + match f.as_mut().poll(cx) { + Poll::Ready(Ok(())) => { + results[ix] = 1; + any_ready = true; } - Err(e) => { + Poll::Ready(Err(e)) => { return Poll::Ready(Err( - anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}")) - )) + e.context(format!("poll_oneoff[{ix}]: {pollable}")) + )); } + Poll::Pending => {} } } if any_ready { Poll::Ready(Ok(results)) } else { - tracing::trace!("overall, we're pending!"); Poll::Pending } } } - let bs = Pin::new(&mut PollOneoff { - table: self.table_mut(), - elems: &pollables, - }) - // TODO: why does poll only get called once? - .await?; - Ok(bs.into_iter().map(|b| if b { 1 } else { 0 }).collect()) + Ok(PollOneoff { + elems: pollables + .iter() + .enumerate() + .map( + |(ix, pollable)| match self.table_mut().get_host_pollable_mut(*pollable) { + Ok(mkf) => Ok((*pollable, mkf.0())), + Err(e) => Err(anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}"))), + }, + ) + .collect::>>()?, + } + .await?) } } pub mod sync { - use std::future::Future; use crate::preview2::{ wasi::poll::poll::Host as AsyncHost, wasi::sync_io::poll::poll::{self, Pollable}, WasiView, }; use anyhow::Result; + use std::future::Future; use tokio::runtime::{Builder, Handle, Runtime}; pub fn block_on(f: F) -> F::Output { From 444d011b584808a79887a7d70280f16e61ec9240 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 15 Jun 2023 15:33:52 -0700 Subject: [PATCH 014/118] Remove TODO for HostOutputStream impl for WritePipe --- crates/wasi/src/preview2/pipe.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index b7085bf43c7b..3b4214ac7bd6 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -209,10 +209,9 @@ impl HostOutputStream for WritePipe { } fn pollable(&self) -> HostPollable { - // TODO(elliottt): because this is using only a `Write` constraint, and that trait doesn't - // provide a way to check if there's space available, this should trivially return that - // there's space to write. - HostPollable::new(|| Box::pin(async { todo!("pollable on a WritePipe") })) - // FIXME + // NOTE: as we only really know that W is Write, there's no way to determine what space is + // available for writing. Thus we indicate that there's space available by returning + // immediately. + HostPollable::new(|| Box::pin(async { Ok(()) })) } } From 8ee7f35aeda7cdc131b9a869b7d9088a5955b2f2 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 15 Jun 2023 16:49:46 -0700 Subject: [PATCH 015/118] Implement pollable for ReadPipe --- crates/wasi/src/preview2/pipe.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 3b4214ac7bd6..179897345296 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -8,7 +8,7 @@ //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; -use anyhow::Error; +use anyhow::{anyhow, Error}; use std::any::Any; use std::convert::TryInto; use std::io::{self, Read, Write}; @@ -119,11 +119,28 @@ impl HostInputStream for ReadPipe { } fn pollable(&self) -> HostPollable { - // TODO(elliottt): this should probably be: - // 1. take the lock - // 2. check if there's anything inside of it to be read - HostPollable::new(|| Box::pin(async { todo!("pollable on a ReadPipe") })) - // FIXME + let reader = Arc::clone(&self.reader); + HostPollable::new(move || { + let reader = Arc::clone(&reader); + Box::pin(async move { + loop { + let amount = match reader.read() { + Ok(g) => g.num_ready_bytes()?, + Err(_) => { + // TODO(elliottt): are there any circumstances where we want to clear + // the poisoned state of the pipe? + return Err(anyhow!("pipe has been poisoned")); + } + }; + if amount > 0 { + return Ok(()); + } + + // TODO(elliottt): is there a better way to wait on the pipe to become ready? + tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; + } + }) + }) } } From ff4e3143620284561114841cb1a78567a52188ea Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 16 Jun 2023 13:48:21 -0700 Subject: [PATCH 016/118] Use a Notify when ReadPipe is ready --- crates/wasi/src/preview2/pipe.rs | 59 ++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 179897345296..56da993aadf3 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -32,12 +32,14 @@ use system_interface::io::ReadReady; /// ``` #[derive(Debug)] pub struct ReadPipe { + notify: Arc, reader: Arc>, } impl Clone for ReadPipe { fn clone(&self) -> Self { Self { + notify: self.notify.clone(), reader: self.reader.clone(), } } @@ -55,7 +57,11 @@ impl ReadPipe { /// /// All `Handle` read operations delegate to reading from this underlying reader. pub fn from_shared(reader: Arc>) -> Self { - Self { reader } + Self { + // TODO(elliottt): should the shared notify be an argument as well? + notify: Arc::new(tokio::sync::Notify::new()), + reader, + } } /// Try to convert this `ReadPipe` back to the underlying `R` type. @@ -119,25 +125,50 @@ impl HostInputStream for ReadPipe { } fn pollable(&self) -> HostPollable { + // This is a standalone function because RwLockReadGuard does not implement Send -- calling + // `reader.read()` from within the async closure below is just not possible. + fn ready(reader: &RwLock) -> bool { + if let Ok(g) = reader.read() { + if let Ok(n) = g.num_ready_bytes() { + return n > 0; + } + } + + // If either read or num_ready_bytes raised an error, we want to consider the pipe + // ready for reading. + true + } + + let notify = Arc::clone(&self.notify); let reader = Arc::clone(&self.reader); HostPollable::new(move || { + // TODO(elliottt): is it possible to avoid these clones? They're needed because `Arc` + // isn't copy, and we need to move values into the async closure. + let notify = Arc::clone(¬ify); let reader = Arc::clone(&reader); Box::pin(async move { - loop { - let amount = match reader.read() { - Ok(g) => g.num_ready_bytes()?, - Err(_) => { - // TODO(elliottt): are there any circumstances where we want to clear - // the poisoned state of the pipe? - return Err(anyhow!("pipe has been poisoned")); + { + let reader = reader.clone(); + let sender = notify.clone(); + tokio::spawn(async move { + while !ready(&reader) { + tokio::task::yield_now().await; } - }; - if amount > 0 { - return Ok(()); - } - // TODO(elliottt): is there a better way to wait on the pipe to become ready? - tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; + sender.notify_one(); + }); + } + + notify.notified().await; + + let g = match reader.read() { + Ok(g) => g, + Err(_) => return Err(anyhow!("pipe has been poisoned")), + }; + + match g.num_ready_bytes() { + Ok(_) => Ok(()), + Err(e) => Err(anyhow!(e)), } }) }) From e5b1aff8fbba58e7d1af9b0488052b04a8ccc933 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 08:46:46 -0700 Subject: [PATCH 017/118] wip --- crates/wasi/src/preview2/pipe.rs | 280 +++++------------------------ crates/wasi/src/preview2/stream.rs | 38 ++-- 2 files changed, 67 insertions(+), 251 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 56da993aadf3..ed0adf5ee6bd 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -8,258 +8,70 @@ //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; -use anyhow::{anyhow, Error}; -use std::any::Any; -use std::convert::TryInto; -use std::io::{self, Read, Write}; -use std::sync::{Arc, RwLock}; -use system_interface::io::ReadReady; - -/// A virtual pipe read end. -/// -/// This reads from a source that implements the [`Read`] trait. It -/// also requires the [`ReadReady`] trait, which is implemented for many -/// popular `Read`-implementing types and is easy to implemented for new -/// types. -/// -/// A variety of `From` impls are provided so that common pipe types are -/// easy to create. For example: -/// -/// ``` -/// use wasmtime_wasi::preview2::{pipe::ReadPipe, WasiCtx}; -/// let stdin = ReadPipe::from("hello from stdin!"); -/// let builder = WasiCtx::builder().set_stdin(stdin); -/// ``` -#[derive(Debug)] -pub struct ReadPipe { - notify: Arc, - reader: Arc>, +use std::sync::{Arc, Mutex}; + +pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { + let (writer, reader) = tokio::sync::mpsc::channel(bound); + + let input = InnerInputPipe { + state: StreamState::Open, + buffer: Vec::new(), + channel: reader, + }; + + let output = InnerOutputPipe { + buffer: Vec::new(), + channel: writer, + }; + + ( + InputPipe(Arc::new(Mutex::new(input))), + OutputPipe(Arc::new(Mutex::new(output))), + ) } -impl Clone for ReadPipe { - fn clone(&self) -> Self { - Self { - notify: self.notify.clone(), - reader: self.reader.clone(), - } - } +struct InnerInputPipe { + state: StreamState, + buffer: Vec, + channel: tokio::sync::mpsc::Receiver>, } -impl ReadPipe { - /// Create a new pipe from a `Read` type. - /// - /// All `Handle` read operations delegate to reading from this underlying reader. - pub fn new(r: R) -> Self { - Self::from_shared(Arc::new(RwLock::new(r))) - } +pub struct InputPipe(Arc>); - /// Create a new pipe from a shareable `Read` type. - /// - /// All `Handle` read operations delegate to reading from this underlying reader. - pub fn from_shared(reader: Arc>) -> Self { - Self { - // TODO(elliottt): should the shared notify be an argument as well? - notify: Arc::new(tokio::sync::Notify::new()), - reader, - } - } - - /// Try to convert this `ReadPipe` back to the underlying `R` type. - /// - /// This will fail with `Err(self)` if multiple references to the underlying `R` exist. - pub fn try_into_inner(mut self) -> Result { - match Arc::try_unwrap(self.reader) { - Ok(rc) => Ok(RwLock::into_inner(rc).unwrap()), - Err(reader) => { - self.reader = reader; - Err(self) - } - } - } - fn borrow(&self) -> std::sync::RwLockWriteGuard { - RwLock::write(&self.reader).unwrap() - } -} - -impl From> for ReadPipe>> { - fn from(r: Vec) -> Self { - Self::new(io::Cursor::new(r)) - } -} - -impl From<&[u8]> for ReadPipe>> { - fn from(r: &[u8]) -> Self { - Self::from(r.to_vec()) - } -} - -impl From for ReadPipe> { - fn from(r: String) -> Self { - Self::new(io::Cursor::new(r)) - } -} - -impl From<&str> for ReadPipe> { - fn from(r: &str) -> Self { - Self::from(r.to_string()) - } -} - -#[async_trait::async_trait] -impl HostInputStream for ReadPipe { - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { - match self.borrow().read(buf) { - Ok(0) => Ok((0, true)), - Ok(n) => Ok((n.try_into()?, false)), - Err(e) if e.kind() == io::ErrorKind::Interrupted => Ok((0, false)), - Err(e) => Err(e.into()), - } - } - - async fn skip(&mut self, nelem: u64) -> Result<(u64, bool), Error> { - let num = io::copy( - &mut io::Read::take(&mut *self.borrow(), nelem), - &mut io::sink(), - )?; - Ok((num, num < nelem)) +impl HostInputStream for InputPipe { + fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + let mut i = self.0.lock().unwrap(); + let l = i.buffer.len().min(dest.len()); + let dest = &mut dest[..l]; + dest.copy_from_slice(&i.buffer[..l]); + i.buffer = i.buffer.split_off(l); + Ok((l as u64, i.state)) } fn pollable(&self) -> HostPollable { - // This is a standalone function because RwLockReadGuard does not implement Send -- calling - // `reader.read()` from within the async closure below is just not possible. - fn ready(reader: &RwLock) -> bool { - if let Ok(g) = reader.read() { - if let Ok(n) = g.num_ready_bytes() { - return n > 0; - } - } - - // If either read or num_ready_bytes raised an error, we want to consider the pipe - // ready for reading. - true - } - - let notify = Arc::clone(&self.notify); - let reader = Arc::clone(&self.reader); + let i = Arc::clone(&self.0); HostPollable::new(move || { - // TODO(elliottt): is it possible to avoid these clones? They're needed because `Arc` - // isn't copy, and we need to move values into the async closure. - let notify = Arc::clone(¬ify); - let reader = Arc::clone(&reader); + let i = Arc::clone(&i); Box::pin(async move { - { - let reader = reader.clone(); - let sender = notify.clone(); - tokio::spawn(async move { - while !ready(&reader) { - tokio::task::yield_now().await; - } - - sender.notify_one(); - }); - } - - notify.notified().await; - - let g = match reader.read() { - Ok(g) => g, - Err(_) => return Err(anyhow!("pipe has been poisoned")), - }; - - match g.num_ready_bytes() { - Ok(_) => Ok(()), - Err(e) => Err(anyhow!(e)), + let mut i = i.lock().unwrap(); + match i.channel.recv().await { + None => i.state = StreamState::Closed, + Some(mut buf) => i.buffer.append(&mut buf), } }) }) } } -/// A virtual pipe write end. -/// -/// ```no_run -/// use wasmtime_wasi::preview2::{pipe::WritePipe, WasiCtx, Table}; -/// let mut table = Table::new(); -/// let stdout = WritePipe::new_in_memory(); -/// let mut ctx = WasiCtx::builder().set_stdout(stdout.clone()).build(&mut table).unwrap(); -/// // use ctx and table in an instance, then make sure it is dropped: -/// drop(ctx); -/// drop(table); -/// let contents: Vec = stdout.try_into_inner().expect("sole remaining reference to WritePipe").into_inner(); -/// println!("contents of stdout: {:?}", contents); -/// ``` -#[derive(Debug)] -pub struct WritePipe { - writer: Arc>, -} - -impl Clone for WritePipe { - fn clone(&self) -> Self { - Self { - writer: self.writer.clone(), - } - } -} - -impl WritePipe { - /// Create a new pipe from a `Write` type. - /// - /// All `Handle` write operations delegate to writing to this underlying writer. - pub fn new(w: W) -> Self { - Self::from_shared(Arc::new(RwLock::new(w))) - } - - /// Create a new pipe from a shareable `Write` type. - /// - /// All `Handle` write operations delegate to writing to this underlying writer. - pub fn from_shared(writer: Arc>) -> Self { - Self { writer } - } - - /// Try to convert this `WritePipe` back to the underlying `W` type. - /// - /// This will fail with `Err(self)` if multiple references to the underlying `W` exist. - pub fn try_into_inner(mut self) -> Result { - match Arc::try_unwrap(self.writer) { - Ok(rc) => Ok(RwLock::into_inner(rc).unwrap()), - Err(writer) => { - self.writer = writer; - Err(self) - } - } - } +impl tokio::io::AsyncRead for InputPipe {} - fn borrow(&self) -> std::sync::RwLockWriteGuard { - RwLock::write(&self.writer).unwrap() - } +struct InnerOutputPipe { + buffer: Vec, + channel: tokio::sync::mpsc::Sender>, } -impl WritePipe>> { - /// Create a new writable virtual pipe backed by a `Vec` buffer. - pub fn new_in_memory() -> Self { - Self::new(io::Cursor::new(vec![])) - } -} +pub struct OutputPipe(Arc>); -#[async_trait::async_trait] -// TODO: can we remove the `Any` constraint here? -impl HostOutputStream for WritePipe { - async fn write(&mut self, buf: &[u8]) -> Result { - let n = self.borrow().write(buf)?; - Ok(n.try_into()?) - } - async fn write_zeroes(&mut self, nelem: u64) -> Result { - let num = io::copy( - &mut io::Read::take(io::repeat(0), nelem), - &mut *self.borrow(), - )?; - Ok(num) - } +impl HostOutputStream for OutputPipe {} - fn pollable(&self) -> HostPollable { - // NOTE: as we only really know that W is Write, there's no way to determine what space is - // available for writing. Thus we indicate that there's space available by returning - // immediately. - HostPollable::new(|| Box::pin(async { Ok(()) })) - } -} +impl tokio::io::AsyncWrite for OutputPipe {} diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index a23dbe058e85..1f2880548b9b 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -6,26 +6,31 @@ use anyhow::Error; // Testing could be to send the guest a timestamp and have them check that it's 100ms in the past? // Try putting this in the command tests. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum StreamState { + Open, + Closed, +} + /// An input bytestream. /// /// This is "pseudo" because the real streams will be a type in wit, and /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. -#[async_trait::async_trait] pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes read /// and a flag indicating whether the end of the stream was reached. - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error>; + fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; /// Vectored-I/O form of `read`. - async fn read_vectored<'a>( + fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], - ) -> Result<(u64, bool), Error> { + ) -> Result<(u64, StreamState), Error> { if bufs.len() > 0 { - self.read(bufs.get_mut(0).unwrap()).await + self.read(bufs.get_mut(0).unwrap()) } else { - self.read(&mut []).await + self.read(&mut []) } } @@ -36,13 +41,13 @@ pub trait HostInputStream: Send + Sync { } /// Read bytes from a stream and discard them. - async fn skip(&mut self, nelem: u64) -> Result<(u64, bool), Error> { + fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let mut nread = 0; let mut saw_end = false; // TODO: Optimize by reading more than one byte at a time. for _ in 0..nelem { - let (num, end) = self.read(&mut [0]).await?; + let (num, end) = self.read(&mut [0])?; nread += num; if end { saw_end = true; @@ -62,15 +67,14 @@ pub trait HostInputStream: Send + Sync { /// This is "pseudo" because the real streams will be a type in wit, and /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. -#[async_trait::async_trait] pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. - async fn write(&mut self, _buf: &[u8]) -> Result; + fn write(&mut self, _buf: &[u8]) -> Result; /// Vectored-I/O form of `write`. - async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { + fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { if bufs.len() > 0 { - self.write(bufs.get(0).unwrap()).await + self.write(bufs.get(0).unwrap()) } else { Ok(0) } @@ -83,7 +87,7 @@ pub trait HostOutputStream: Send + Sync { } /// Transfer bytes directly from an input stream to an output stream. - async fn splice( + fn splice( &mut self, src: &mut dyn HostInputStream, nelem: u64, @@ -94,8 +98,8 @@ pub trait HostOutputStream: Send + Sync { // TODO: Optimize by splicing more than one byte at a time. for _ in 0..nelem { let mut buf = [0u8]; - let (num, end) = src.read(&mut buf).await?; - self.write(&buf).await?; + let (num, end) = src.read(&mut buf)?; + self.write(&buf)?; nspliced += num; if end { saw_end = true; @@ -107,12 +111,12 @@ pub trait HostOutputStream: Send + Sync { } /// Repeatedly write a byte to a stream. - async fn write_zeroes(&mut self, nelem: u64) -> Result { + fn write_zeroes(&mut self, nelem: u64) -> Result { let mut nwritten = 0; // TODO: Optimize by writing more than one byte at a time. for _ in 0..nelem { - let num = self.write(&[0]).await?; + let num = self.write(&[0])?; if num == 0 { break; } From 96ea603587464d178f059fb461f130a829cfbb96 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 10:09:03 -0700 Subject: [PATCH 018/118] wip --- crates/wasi/src/preview2/pipe.rs | 66 ++++++++++++++++++++++++------ crates/wasi/src/preview2/stream.rs | 30 +++++++------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index ed0adf5ee6bd..3e105f51f55a 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -8,7 +8,7 @@ //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); @@ -20,13 +20,12 @@ pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { }; let output = InnerOutputPipe { - buffer: Vec::new(), - channel: writer, + channel: SenderState::Channel(writer), }; ( - InputPipe(Arc::new(Mutex::new(input))), - OutputPipe(Arc::new(Mutex::new(output))), + InputPipe(Arc::new(tokio::sync::Mutex::new(input))), + OutputPipe(Arc::new(tokio::sync::Mutex::new(output))), ) } @@ -36,11 +35,12 @@ struct InnerInputPipe { channel: tokio::sync::mpsc::Receiver>, } -pub struct InputPipe(Arc>); +pub struct InputPipe(Arc>); +#[async_trait::async_trait] impl HostInputStream for InputPipe { - fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - let mut i = self.0.lock().unwrap(); + async fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + let mut i = self.0.lock().await; let l = i.buffer.len().min(dest.len()); let dest = &mut dest[..l]; dest.copy_from_slice(&i.buffer[..l]); @@ -53,11 +53,12 @@ impl HostInputStream for InputPipe { HostPollable::new(move || { let i = Arc::clone(&i); Box::pin(async move { - let mut i = i.lock().unwrap(); + let mut i = i.lock().await; match i.channel.recv().await { None => i.state = StreamState::Closed, Some(mut buf) => i.buffer.append(&mut buf), } + Ok(()) }) }) } @@ -65,13 +66,52 @@ impl HostInputStream for InputPipe { impl tokio::io::AsyncRead for InputPipe {} +enum SenderState { + Writable(tokio::sync::OwnedPermit>), + Channel(tokio::sync::mpsc::Sender>), +} + struct InnerOutputPipe { - buffer: Vec, - channel: tokio::sync::mpsc::Sender>, + channel: SenderState, } -pub struct OutputPipe(Arc>); +pub struct OutputPipe(Arc>); + +#[async_trait::async_trait] +impl HostOutputStream for OutputPipe { + async fn write(&mut self, buf: &[u8]) -> Result { + let mut i = self.0.lock().await; + let bytes = Vec::from_iter(buf); + match i.channel { + SenderState::Writable(p) => { + let s = p.send(bytes).await; + i.channel = SenderState::Channel(s); + } -impl HostOutputStream for OutputPipe {} + SenderState::Channel(s) => { + s.send(bytes).await; + } + } + + Ok(buf.len() as u64) + } + + fn pollable(&self) -> HostPollable { + let i = Arc::clone(&self.0); + HostPollable::new(move || { + let i = Arc::clone(&i); + Box::pin(async move { + let mut i = i.lock().await; + match i.channel.reserve_owned() { + Ok(p) => { + i.channel = SenderState::Writable(p); + Ok(()) + } + Err(e) => Err(anyhow!(e)), + } + }) + }) + } +} impl tokio::io::AsyncWrite for OutputPipe {} diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 1f2880548b9b..94a2fd186496 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -17,20 +17,21 @@ pub enum StreamState { /// This is "pseudo" because the real streams will be a type in wit, and /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. +#[async_trait::async_trait] pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes read /// and a flag indicating whether the end of the stream was reached. - fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; + async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; /// Vectored-I/O form of `read`. - fn read_vectored<'a>( + async fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], ) -> Result<(u64, StreamState), Error> { if bufs.len() > 0 { - self.read(bufs.get_mut(0).unwrap()) + self.read(bufs.get_mut(0).unwrap()).await } else { - self.read(&mut []) + self.read(&mut []).await } } @@ -41,13 +42,13 @@ pub trait HostInputStream: Send + Sync { } /// Read bytes from a stream and discard them. - fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { + async fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let mut nread = 0; let mut saw_end = false; // TODO: Optimize by reading more than one byte at a time. for _ in 0..nelem { - let (num, end) = self.read(&mut [0])?; + let (num, end) = self.read(&mut [0]).await?; nread += num; if end { saw_end = true; @@ -67,14 +68,15 @@ pub trait HostInputStream: Send + Sync { /// This is "pseudo" because the real streams will be a type in wit, and /// built into the wit bindings, and will support async and type parameters. /// This pseudo-stream abstraction is synchronous and only supports bytes. +#[async_trait::async_trait] pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, _buf: &[u8]) -> Result; + async fn write(&mut self, _buf: &[u8]) -> Result; /// Vectored-I/O form of `write`. - fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { + async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { if bufs.len() > 0 { - self.write(bufs.get(0).unwrap()) + self.write(bufs.get(0).unwrap()).await } else { Ok(0) } @@ -87,7 +89,7 @@ pub trait HostOutputStream: Send + Sync { } /// Transfer bytes directly from an input stream to an output stream. - fn splice( + async fn splice( &mut self, src: &mut dyn HostInputStream, nelem: u64, @@ -98,8 +100,8 @@ pub trait HostOutputStream: Send + Sync { // TODO: Optimize by splicing more than one byte at a time. for _ in 0..nelem { let mut buf = [0u8]; - let (num, end) = src.read(&mut buf)?; - self.write(&buf)?; + let (num, end) = src.read(&mut buf).await?; + self.write(&buf).await?; nspliced += num; if end { saw_end = true; @@ -111,12 +113,12 @@ pub trait HostOutputStream: Send + Sync { } /// Repeatedly write a byte to a stream. - fn write_zeroes(&mut self, nelem: u64) -> Result { + async fn write_zeroes(&mut self, nelem: u64) -> Result { let mut nwritten = 0; // TODO: Optimize by writing more than one byte at a time. for _ in 0..nelem { - let num = self.write(&[0])?; + let num = self.write(&[0]).await?; if num == 0 { break; } From cdd1dce0682b23a970c6710081b82144355aba9d Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 11:51:56 -0700 Subject: [PATCH 019/118] Read/write pipe ends with tokio channels --- crates/wasi/Cargo.toml | 2 +- crates/wasi/src/preview2/ctx.rs | 9 +- crates/wasi/src/preview2/filesystem.rs | 18 ++- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/pipe.rs | 127 +++++++++++++----- .../wasi/src/preview2/preview2/filesystem.rs | 4 +- crates/wasi/src/preview2/preview2/io.rs | 6 +- crates/wasi/src/preview2/stdio.rs | 29 ++-- crates/wasi/src/preview2/stream.rs | 28 ++-- 9 files changed, 153 insertions(+), 72 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index d3e92227ed45..74247d390d33 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -22,7 +22,7 @@ wiggle = { workspace = true, optional = true } libc = { workspace = true } once_cell = { workspace = true } -tokio = { workspace = true, optional = true, features = ["time"] } +tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "rt"] } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 4cb322409508..30b97766841b 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -37,10 +37,11 @@ impl WasiCtxBuilder { let mut result = Self::default() .set_clocks(clocks::host::clocks_ctx()) .set_insecure_random(insecure_random) - .set_insecure_random_seed(insecure_random_seed) - .set_stdin(pipe::ReadPipe::new(std::io::empty())) - .set_stdout(pipe::WritePipe::new(std::io::sink())) - .set_stderr(pipe::WritePipe::new(std::io::sink())); + .set_insecure_random_seed(insecure_random_seed); + // TODO: fix these + // .set_stdin(pipe::ReadPipe::new(std::io::empty())) + // .set_stdout(pipe::WritePipe::new(std::io::sink())) + // .set_stderr(pipe::WritePipe::new(std::io::sink())); result.random = Some(random::thread_rng()); result } diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index b0e413af5e7b..9fd3b7293ed9 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,4 +1,6 @@ -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, Table, TableError}; +use crate::preview2::{ + HostInputStream, HostOutputStream, HostPollable, StreamState, Table, TableError, +}; use std::sync::Arc; bitflags::bitflags! { @@ -96,7 +98,7 @@ impl FileInputStream { #[async_trait::async_trait] impl HostInputStream for FileInputStream { - async fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, bool)> { + async fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; let (n, end) = read_result(self.file.read_at(buf, self.position))?; self.position = self.position.wrapping_add(n); @@ -105,7 +107,7 @@ impl HostInputStream for FileInputStream { async fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], - ) -> anyhow::Result<(u64, bool)> { + ) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; let (n, end) = read_result(self.file.read_vectored_at(bufs, self.position))?; self.position = self.position.wrapping_add(n); @@ -120,11 +122,13 @@ impl HostInputStream for FileInputStream { } } -pub(crate) fn read_result(r: Result) -> Result<(u64, bool), std::io::Error> { +pub(crate) fn read_result( + r: Result, +) -> Result<(u64, StreamState), std::io::Error> { match r { - Ok(0) => Ok((0, true)), - Ok(n) => Ok((n as u64, false)), - Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok((0, false)), + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n as u64, StreamState::Open)), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), Err(e) => Err(e), } } diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 8ac170307b1d..571e24fc156a 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -39,7 +39,7 @@ pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; -pub use self::stream::{HostInputStream, HostOutputStream, TableStreamExt}; +pub use self::stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}; pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 3e105f51f55a..26ca56ae9f56 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -7,25 +7,20 @@ //! Some convenience constructors are included for common backing types like `Vec` and `String`, //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; +use anyhow::{anyhow, Error}; use std::sync::Arc; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); - let input = InnerInputPipe { - state: StreamState::Open, - buffer: Vec::new(), - channel: reader, - }; - - let output = InnerOutputPipe { - channel: SenderState::Channel(writer), - }; - ( - InputPipe(Arc::new(tokio::sync::Mutex::new(input))), - OutputPipe(Arc::new(tokio::sync::Mutex::new(output))), + InputPipe(Arc::new(tokio::sync::Mutex::new(InnerInputPipe::new( + reader, + )))), + OutputPipe(Arc::new(tokio::sync::Mutex::new(InnerOutputPipe::new( + writer, + )))), ) } @@ -35,6 +30,16 @@ struct InnerInputPipe { channel: tokio::sync::mpsc::Receiver>, } +impl InnerInputPipe { + fn new(channel: tokio::sync::mpsc::Receiver>) -> Self { + Self { + state: StreamState::Open, + buffer: Vec::new(), + channel, + } + } +} + pub struct InputPipe(Arc>); #[async_trait::async_trait] @@ -64,15 +69,59 @@ impl HostInputStream for InputPipe { } } -impl tokio::io::AsyncRead for InputPipe {} +// impl tokio::io::AsyncRead for InputPipe {} enum SenderState { - Writable(tokio::sync::OwnedPermit>), + Writable(tokio::sync::mpsc::OwnedPermit>), Channel(tokio::sync::mpsc::Sender>), } struct InnerOutputPipe { - channel: SenderState, + buffer: Vec, + channel: Option, +} + +impl InnerOutputPipe { + fn new(s: tokio::sync::mpsc::Sender>) -> Self { + Self { + buffer: Vec::new(), + channel: Some(SenderState::Channel(s)), + } + } + + async fn blocking_send(&mut self, buf: Vec) -> Result<(), Error> { + let s = match self.take_channel() { + SenderState::Writable(p) => { + let s = p.send(buf); + SenderState::Channel(s) + } + + SenderState::Channel(s) => { + s.send(buf).await?; + SenderState::Channel(s) + } + }; + + self.channel = Some(s); + + Ok(()) + } + + async fn flush(&mut self) { + if self.buffer.is_empty() { + return; + } + + let bytes = core::mem::take(&mut self.buffer); + + self.blocking_send(bytes) + .await + .expect("fixme: handle closed write end later") + } + + fn take_channel(&mut self) -> SenderState { + self.channel.take().expect("Missing channel state") + } } pub struct OutputPipe(Arc>); @@ -80,18 +129,30 @@ pub struct OutputPipe(Arc>); #[async_trait::async_trait] impl HostOutputStream for OutputPipe { async fn write(&mut self, buf: &[u8]) -> Result { + use tokio::sync::mpsc::error::TrySendError; + let mut i = self.0.lock().await; - let bytes = Vec::from_iter(buf); - match i.channel { + let mut bytes = core::mem::take(&mut i.buffer); + bytes.extend(buf); + let (s, bytes) = match i.take_channel() { SenderState::Writable(p) => { - let s = p.send(bytes).await; - i.channel = SenderState::Channel(s); + let s = p.send(bytes); + (s, Vec::new()) } - SenderState::Channel(s) => { - s.send(bytes).await; - } - } + SenderState::Channel(s) => match s.try_send(bytes) { + Ok(()) => (s, Vec::new()), + Err(TrySendError::Full(b)) => (s, b), + Err(TrySendError::Closed(_)) => { + // TODO: we may need to communicate failure out in a way that doesn't result in + // a trap. + return Err(anyhow!("pipe closed")); + } + }, + }; + + i.buffer = bytes; + i.channel = Some(SenderState::Channel(s)); Ok(buf.len() as u64) } @@ -102,16 +163,18 @@ impl HostOutputStream for OutputPipe { let i = Arc::clone(&i); Box::pin(async move { let mut i = i.lock().await; - match i.channel.reserve_owned() { - Ok(p) => { - i.channel = SenderState::Writable(p); - Ok(()) - } - Err(e) => Err(anyhow!(e)), - } + i.flush().await; + let p = match i.channel.take().expect("Missing sender channel state") { + SenderState::Writable(p) => p, + SenderState::Channel(s) => s.reserve_owned().await?, + }; + + i.channel = Some(SenderState::Writable(p)); + + Ok(()) }) }) } } -impl tokio::io::AsyncWrite for OutputPipe {} +// impl tokio::io::AsyncWrite for OutputPipe {} diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 5483de2d00b2..7d1caa85c623 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -196,7 +196,7 @@ impl filesystem::Host for T { } let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; - let (bytes_read, end) = crate::preview2::filesystem::read_result( + let (bytes_read, state) = crate::preview2::filesystem::read_result( f.file .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset), )?; @@ -207,7 +207,7 @@ impl filesystem::Host for T { .expect("bytes read into memory as u64 fits in usize"), ); - Ok((buffer, end)) + Ok((buffer, state.is_closed())) } fn write( diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index da994cfd03dd..41cbc6d1c4d5 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -54,11 +54,11 @@ impl streams::Host for T { let buffer_len = std::cmp::min(len, 0x400000) as _; let mut buffer = vec![0; buffer_len]; - let (bytes_read, end) = s.read(&mut buffer).await?; + let (bytes_read, state) = s.read(&mut buffer).await?; buffer.truncate(bytes_read as usize); - Ok((buffer, end)) + Ok((buffer, state.is_closed())) } async fn blocking_read( @@ -92,7 +92,7 @@ impl streams::Host for T { let (bytes_skipped, end) = s.skip(len).await?; - Ok((bytes_skipped, end)) + Ok((bytes_skipped, end.is_closed())) } async fn blocking_skip( diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 52af008ce388..6dcc25f4c51a 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,7 +2,7 @@ use anyhow::Error; use std::convert::TryInto; use std::io::{self, Read, Write}; -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; pub struct Stdin(std::io::Stdin); @@ -12,22 +12,22 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl HostInputStream for Stdin { - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { + async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { match Read::read(&mut self.0, buf) { - Ok(0) => Ok((0, true)), - Ok(n) => Ok((n as u64, false)), - Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, false)), + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n as u64, StreamState::Open)), + Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), Err(err) => Err(err.into()), } } async fn read_vectored<'a>( &mut self, bufs: &mut [io::IoSliceMut<'a>], - ) -> Result<(u64, bool), Error> { + ) -> Result<(u64, StreamState), Error> { match Read::read_vectored(&mut self.0, bufs) { - Ok(0) => Ok((0, true)), - Ok(n) => Ok((n as u64, false)), - Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, false)), + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n as u64, StreamState::Open)), + Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), Err(err) => Err(err.into()), } } @@ -37,9 +37,16 @@ impl HostInputStream for Stdin { } */ - async fn skip(&mut self, nelem: u64) -> Result<(u64, bool), Error> { + async fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let num = io::copy(&mut io::Read::take(&mut self.0, nelem), &mut io::sink())?; - Ok((num, num < nelem)) + Ok(( + num, + if num < nelem { + StreamState::Closed + } else { + StreamState::Open + }, + )) } fn pollable(&self) -> HostPollable { diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 94a2fd186496..5d03e3fe1c02 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -12,6 +12,12 @@ pub enum StreamState { Closed, } +impl StreamState { + pub fn is_closed(&self) -> bool { + *self == Self::Closed + } +} + /// An input bytestream. /// /// This is "pseudo" because the real streams will be a type in wit, and @@ -44,19 +50,19 @@ pub trait HostInputStream: Send + Sync { /// Read bytes from a stream and discard them. async fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let mut nread = 0; - let mut saw_end = false; + let mut state = StreamState::Open; // TODO: Optimize by reading more than one byte at a time. for _ in 0..nelem { - let (num, end) = self.read(&mut [0]).await?; + let (num, read_state) = self.read(&mut [0]).await?; nread += num; - if end { - saw_end = true; + if read_state.is_closed() { + state = read_state; break; } } - Ok((nread, saw_end)) + Ok((nread, state)) } /// Get the Pollable implementation for read readiness. @@ -93,23 +99,23 @@ pub trait HostOutputStream: Send + Sync { &mut self, src: &mut dyn HostInputStream, nelem: u64, - ) -> Result<(u64, bool), Error> { + ) -> Result<(u64, StreamState), Error> { let mut nspliced = 0; - let mut saw_end = false; + let mut state = StreamState::Open; // TODO: Optimize by splicing more than one byte at a time. for _ in 0..nelem { let mut buf = [0u8]; - let (num, end) = src.read(&mut buf).await?; + let (num, read_state) = src.read(&mut buf).await?; self.write(&buf).await?; nspliced += num; - if end { - saw_end = true; + if read_state.is_closed() { + state = read_state; break; } } - Ok((nspliced, saw_end)) + Ok((nspliced, state)) } /// Repeatedly write a byte to a stream. From 8aa1ca89e6ae4e3abdc9a902c35ed4d7052da778 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 13:57:56 -0700 Subject: [PATCH 020/118] Empty reader/writer wrappers --- crates/wasi/src/preview2/pipe.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 26ca56ae9f56..de90962729ff 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -24,6 +24,28 @@ pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { ) } +pub async fn empty_output() -> OutputPipe { + let (writer, reader) = pipe(1); + + tokio::spawn(async move { + let mut i = writer.0.lock().await; + while let Some(_) = i.channel.recv().await {} + }); + + reader +} + +pub async fn sink_input() -> InputPipe { + let (writer, reader) = pipe(1); + + tokio::spawn(async move { + let mut i = reader.0.lock().await; + while !i.blocking_send(Vec::new()).await.is_err() {} + }); + + writer +} + struct InnerInputPipe { state: StreamState, buffer: Vec, From f1b93a230dda5f478c70592ac85bce88ef8fc597 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 15:29:11 -0700 Subject: [PATCH 021/118] EmptyStream, and warning cleanup --- crates/wasi/src/preview2/ctx.rs | 11 ++++--- crates/wasi/src/preview2/pipe.rs | 22 -------------- crates/wasi/src/preview2/preview2/io.rs | 8 ++---- crates/wasi/src/preview2/stdio.rs | 38 +++++++++++++++++++++++++ crates/wasi/src/preview2/wasi/mod.rs | 2 +- 5 files changed, 47 insertions(+), 34 deletions(-) diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 30b97766841b..631544ae2785 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -1,7 +1,7 @@ use crate::preview2::{ clocks::{self, WasiClocks}, filesystem::{Dir, TableFsExt}, - pipe, random, stdio, + random, stdio, stream::{HostInputStream, HostOutputStream, TableStreamExt}, DirPerms, FilePerms, Table, }; @@ -37,11 +37,10 @@ impl WasiCtxBuilder { let mut result = Self::default() .set_clocks(clocks::host::clocks_ctx()) .set_insecure_random(insecure_random) - .set_insecure_random_seed(insecure_random_seed); - // TODO: fix these - // .set_stdin(pipe::ReadPipe::new(std::io::empty())) - // .set_stdout(pipe::WritePipe::new(std::io::sink())) - // .set_stderr(pipe::WritePipe::new(std::io::sink())); + .set_insecure_random_seed(insecure_random_seed) + .set_stdin(stdio::EmptyStream) + .set_stdout(stdio::EmptyStream) + .set_stderr(stdio::EmptyStream); result.random = Some(random::thread_rng()); result } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index de90962729ff..26ca56ae9f56 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -24,28 +24,6 @@ pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { ) } -pub async fn empty_output() -> OutputPipe { - let (writer, reader) = pipe(1); - - tokio::spawn(async move { - let mut i = writer.0.lock().await; - while let Some(_) = i.channel.recv().await {} - }); - - reader -} - -pub async fn sink_input() -> InputPipe { - let (writer, reader) = pipe(1); - - tokio::spawn(async move { - let mut i = reader.0.lock().await; - while !i.blocking_send(Vec::new()).await.is_err() {} - }); - - writer -} - struct InnerInputPipe { state: StreamState, buffer: Vec, diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 41cbc6d1c4d5..b213d1f8b242 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -210,13 +210,11 @@ impl streams::Host for T { pub mod sync { use crate::preview2::{ poll::sync::block_on, - stream::{HostInputStream, HostOutputStream, TableStreamExt}, - wasi::io::streams::{Host as AsyncHost, StreamError as AsyncStreamError}, - wasi::sync_io::io::streams::{self, InputStream, OutputStream, StreamError}, + wasi::io::streams::Host as AsyncHost, + wasi::sync_io::io::streams::{self, InputStream, OutputStream}, wasi::sync_io::poll::poll::Pollable, - TableError, TablePollableExt, WasiView, + WasiView, }; - use anyhow::anyhow; impl streams::Host for T { fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 6dcc25f4c51a..11f7b86bf7ba 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -103,3 +103,41 @@ pub fn stderr() -> Stderr { Stderr(std::io::stderr()) } wasi_output_stream_impl!(Stderr, Stderr); + +pub struct EmptyStream; + +#[async_trait::async_trait] +impl HostInputStream for EmptyStream { + async fn read(&mut self, _buf: &mut [u8]) -> Result<(u64, StreamState), Error> { + Ok((0, StreamState::Open)) + } + + fn pollable(&self) -> HostPollable { + struct Never; + + impl std::future::Future for Never { + type Output = anyhow::Result<()>; + fn poll( + self: std::pin::Pin<&mut Self>, + _ctx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + std::task::Poll::Pending + } + } + + // This stream is never ready for reading. + HostPollable::new(|| Box::pin(Never)) + } +} + +#[async_trait::async_trait] +impl HostOutputStream for EmptyStream { + async fn write(&mut self, buf: &[u8]) -> Result { + Ok(buf.len() as u64) + } + + fn pollable(&self) -> HostPollable { + // This stream is always ready for writing. + HostPollable::new(|| Box::pin(async { Ok(()) })) + } +} diff --git a/crates/wasi/src/preview2/wasi/mod.rs b/crates/wasi/src/preview2/wasi/mod.rs index 63f99c6e2f18..28306c237003 100644 --- a/crates/wasi/src/preview2/wasi/mod.rs +++ b/crates/wasi/src/preview2/wasi/mod.rs @@ -17,7 +17,7 @@ pub mod sync_io { pub use self::_internal::wasi::{io, poll}; impl From for io::streams::StreamError { - fn from(other: super::io::streams::StreamError) -> Self { + fn from(_other: super::io::streams::StreamError) -> Self { // There are no cases for this record. Self {} } From 9680e7eff3bee7d8e7ea7becfdbe9de0ac2df205 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 17:16:48 -0700 Subject: [PATCH 022/118] Wrapped reader/writer structs --- crates/wasi/Cargo.toml | 2 +- crates/wasi/src/preview2/pipe.rs | 133 +++++++++++++++++++++++++++++- crates/wasi/src/preview2/stdio.rs | 10 +++ 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 74247d390d33..dc5e017cc1f5 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -22,7 +22,7 @@ wiggle = { workspace = true, optional = true } libc = { workspace = true } once_cell = { workspace = true } -tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "rt"] } +tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt"] } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 26ca56ae9f56..e5425f32431c 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -10,6 +10,7 @@ use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; use anyhow::{anyhow, Error}; use std::sync::Arc; +use tokio::sync::Mutex; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); @@ -69,7 +70,89 @@ impl HostInputStream for InputPipe { } } -// impl tokio::io::AsyncRead for InputPipe {} +struct InnerWrappedRead { + state: StreamState, + buffer: Vec, + reader: T, +} + +pub struct WrappedRead(Arc>>); + +impl WrappedRead { + pub fn new(reader: T) -> Self { + Self(Arc::new(Mutex::new(InnerWrappedRead { + state: StreamState::Open, + buffer: Vec::new(), + reader, + }))) + } +} + +#[async_trait::async_trait] +impl HostInputStream for WrappedRead { + async fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + use std::io::Write; + use tokio::io::AsyncReadExt; + let mut i = self.0.lock().await; + let l = dest.write(&i.buffer)?; + + i.buffer.drain(..l); + if !i.buffer.is_empty() { + return Ok((l as u64, StreamState::Open)); + } + + if i.state.is_closed() { + return Ok((l as u64, StreamState::Closed)); + } + + let mut dest = &mut dest[l..]; + let rest = if !dest.is_empty() { + let written = i.reader.read_buf(&mut dest).await?; + if written == 0 { + i.state = StreamState::Closed; + } + written + } else { + 0 + }; + + // TODO: figure out how we're tracking the stream state. Maybe mutate it when handling the + // result of `read_buf`? + Ok(((l + rest) as u64, i.state)) + } + + fn pollable(&self) -> HostPollable { + use tokio::io::AsyncReadExt; + let i = Arc::clone(&self.0); + HostPollable::new(move || { + let i = Arc::clone(&i); + Box::pin(async move { + let mut i = i.lock().await; + + if i.state.is_closed() { + return Ok(()); + } + + let mut bytes = core::mem::take(&mut i.buffer); + let start = bytes.len(); + bytes.resize(start + 1024, 0); + let l = i.reader.read_buf(&mut &mut bytes[start..]).await?; + + // Reading 0 bytes means either there wasn't enough space in the buffer (which we + // know there is because we just resized) or that the stream has closed. Thus, we + // know the stream has closed here. + if l == 0 { + i.state = StreamState::Closed; + } + + bytes.drain(start + l..); + i.buffer = bytes; + + Ok(()) + }) + }) + } +} enum SenderState { Writable(tokio::sync::mpsc::OwnedPermit>), @@ -177,4 +260,50 @@ impl HostOutputStream for OutputPipe { } } -// impl tokio::io::AsyncWrite for OutputPipe {} +struct InnerWrappedWrite { + buffer: Vec, + writer: T, +} + +pub struct WrappedWrite(Arc>>); + +impl WrappedWrite { + pub fn new(writer: T) -> Self { + WrappedWrite(Arc::new(Mutex::new(InnerWrappedWrite { + buffer: Vec::new(), + writer, + }))) + } +} + +#[async_trait::async_trait] +impl HostOutputStream + for WrappedWrite +{ + async fn write(&mut self, buf: &[u8]) -> Result { + use tokio::io::AsyncWriteExt; + let mut i = self.0.lock().await; + let mut bytes = core::mem::take(&mut i.buffer); + bytes.extend(buf); + let written = i.writer.write_buf(&mut bytes.as_slice()).await?; + bytes.drain(..written); + i.buffer = bytes; + Ok(written as u64) + } + + fn pollable(&self) -> HostPollable { + use tokio::io::AsyncWriteExt; + let i = Arc::clone(&self.0); + HostPollable::new(move || { + let i = Arc::clone(&i); + Box::pin(async move { + let mut i = i.lock().await; + let bytes = core::mem::take(&mut i.buffer); + if !bytes.is_empty() { + i.writer.write_all(bytes.as_slice()).await?; + } + Ok(()) + }) + }) + } +} diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 11f7b86bf7ba..846ae3154c73 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -49,6 +49,16 @@ impl HostInputStream for Stdin { )) } + #[cfg(not(windows))] + fn pollable(&self) -> HostPollable { + // TODO(elliottt): this can be a read with an empty buffer to check for ready, but on + // windows there is a special function that needs to be called in a worker thread, as stdin + // is special. There is already code in wasi-common for creating the worker thread, copy + // that. + HostPollable::new(|| Box::pin(async { todo!("pollable on stdin") })) + } + + #[cfg(windows)] fn pollable(&self) -> HostPollable { // TODO(elliottt): this can be a read with an empty buffer to check for ready, but on // windows there is a special function that needs to be called in a worker thread, as stdin From c76cce52ae88a5c7b9d20bb2e476b5c5014b3f19 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 21 Jun 2023 17:24:12 -0700 Subject: [PATCH 023/118] Rework stdio in terms of wrapped read/write --- crates/wasi/src/preview2/stdio.rs | 109 +++--------------------------- 1 file changed, 8 insertions(+), 101 deletions(-) diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 846ae3154c73..aa652df8560c 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,118 +1,25 @@ use anyhow::Error; -use std::convert::TryInto; -use std::io::{self, Read, Write}; use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; +use crate::preview2::pipe::{WrappedRead, WrappedWrite}; -pub struct Stdin(std::io::Stdin); +// TODO: different cfg for windows here +pub type Stdin = WrappedRead; pub fn stdin() -> Stdin { - Stdin(std::io::stdin()) + WrappedRead::new(tokio::io::stdin()) } -#[async_trait::async_trait] -impl HostInputStream for Stdin { - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { - match Read::read(&mut self.0, buf) { - Ok(0) => Ok((0, StreamState::Closed)), - Ok(n) => Ok((n as u64, StreamState::Open)), - Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), - Err(err) => Err(err.into()), - } - } - async fn read_vectored<'a>( - &mut self, - bufs: &mut [io::IoSliceMut<'a>], - ) -> Result<(u64, StreamState), Error> { - match Read::read_vectored(&mut self.0, bufs) { - Ok(0) => Ok((0, StreamState::Closed)), - Ok(n) => Ok((n as u64, StreamState::Open)), - Err(err) if err.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), - Err(err) => Err(err.into()), - } - } - /* this method can be implemented once `can_vector` stabilizes in std: - fn is_read_vectored(&self) -> bool { - Read::is_read_vectored(&mut self.0) - } - */ - - async fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { - let num = io::copy(&mut io::Read::take(&mut self.0, nelem), &mut io::sink())?; - Ok(( - num, - if num < nelem { - StreamState::Closed - } else { - StreamState::Open - }, - )) - } - - #[cfg(not(windows))] - fn pollable(&self) -> HostPollable { - // TODO(elliottt): this can be a read with an empty buffer to check for ready, but on - // windows there is a special function that needs to be called in a worker thread, as stdin - // is special. There is already code in wasi-common for creating the worker thread, copy - // that. - HostPollable::new(|| Box::pin(async { todo!("pollable on stdin") })) - } - - #[cfg(windows)] - fn pollable(&self) -> HostPollable { - // TODO(elliottt): this can be a read with an empty buffer to check for ready, but on - // windows there is a special function that needs to be called in a worker thread, as stdin - // is special. There is already code in wasi-common for creating the worker thread, copy - // that. - HostPollable::new(|| Box::pin(async { todo!("pollable on stdin") })) - } -} - -macro_rules! wasi_output_stream_impl { - ($ty:ty, $ident:ident) => { - #[async_trait::async_trait] - impl HostOutputStream for $ty { - async fn write(&mut self, buf: &[u8]) -> Result { - let n = Write::write(&mut self.0, buf)?; - Ok(n.try_into()?) - } - async fn write_vectored<'a>(&mut self, bufs: &[io::IoSlice<'a>]) -> Result { - let n = Write::write_vectored(&mut self.0, bufs)?; - Ok(n.try_into()?) - } - /* this method can be implemented once `can_vector` stablizes in std - fn is_write_vectored(&self) -> bool { - Write::is_write_vectored(&mut self.0) - } - */ - async fn write_zeroes(&mut self, nelem: u64) -> Result { - let num = io::copy(&mut io::Read::take(io::repeat(0), nelem), &mut self.0)?; - Ok(num) - } - - fn pollable(&self) -> HostPollable { - // TODO(elliottt): not clear how to implement this, but writing an empty buffer is - // probably the right next step. It's not clear how stdout/stderr could not be - // ready for writing. - HostPollable::new(|| Box::pin(async { todo!("pollable on stdio, stderr writes") })) - } - } - }; -} - -pub struct Stdout(std::io::Stdout); +pub type Stdout = WrappedWrite; pub fn stdout() -> Stdout { - Stdout(std::io::stdout()) + WrappedWrite::new(tokio::io::stdout()) } -wasi_output_stream_impl!(Stdout, Stdout); - -pub struct Stderr(std::io::Stderr); +pub type Stderr = WrappedWrite; pub fn stderr() -> Stderr { - Stderr(std::io::stderr()) + WrappedWrite::new(tokio::io::stderr()) } -wasi_output_stream_impl!(Stderr, Stderr); pub struct EmptyStream; From a5151e9287199424dc0c798412ddec7752b3299b Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 22 Jun 2023 10:08:37 -0700 Subject: [PATCH 024/118] Add MemoryOutputPipe and update tests --- .../tests/wasi-preview2-components-sync.rs | 18 +++----- .../tests/wasi-preview2-components.rs | 18 +++----- crates/wasi/src/preview2/pipe.rs | 41 +++++++++++++++++++ crates/wasi/src/preview2/stdio.rs | 2 +- 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index d80e1e080f5c..3fe4ec209813 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -3,7 +3,7 @@ use anyhow::Result; use tempfile::TempDir; use wasmtime::{component::Linker, Config, Engine, Store}; use wasmtime_wasi::preview2::{ - pipe::WritePipe, + pipe::MemoryOutputPipe, wasi::command::sync::{add_to_linker, Command}, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, }; @@ -30,8 +30,8 @@ pub fn prepare_workspace(exe_name: &str) -> Result { fn run(name: &str, inherit_stdio: bool) -> Result<()> { let workspace = prepare_workspace(name)?; - let stdout = WritePipe::new_in_memory(); - let stderr = WritePipe::new_in_memory(); + let stdout = MemoryOutputPipe::new(); + let stderr = MemoryOutputPipe::new(); let r = { let mut linker = Linker::new(&ENGINE); add_to_linker(&mut linker)?; @@ -87,17 +87,11 @@ fn run(name: &str, inherit_stdio: bool) -> Result<()> { }; r.map_err(move |trap: anyhow::Error| { - let stdout = stdout - .try_into_inner() - .expect("sole ref to stdout") - .into_inner(); + let stdout = stdout.finalize(); if !stdout.is_empty() { println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); } - let stderr = stderr - .try_into_inner() - .expect("sole ref to stderr") - .into_inner(); + let stderr = stderr.finalize(); if !stderr.is_empty() { println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); } @@ -252,8 +246,6 @@ fn poll_oneoff_files() { run("poll_oneoff_files", false).unwrap() } #[test_log::test] -// This is a known bug with the preview 2 implementation: -#[should_panic] fn poll_oneoff_stdio() { run("poll_oneoff_stdio", true).unwrap() } diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index 2b65ffaf718b..b4f195113306 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -3,7 +3,7 @@ use anyhow::Result; use tempfile::TempDir; use wasmtime::{component::Linker, Config, Engine, Store}; use wasmtime_wasi::preview2::{ - pipe::WritePipe, + pipe::MemoryOutputPipe, wasi::command::{add_to_linker, Command}, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, }; @@ -30,8 +30,8 @@ pub fn prepare_workspace(exe_name: &str) -> Result { async fn run(name: &str, inherit_stdio: bool) -> Result<()> { let workspace = prepare_workspace(name)?; - let stdout = WritePipe::new_in_memory(); - let stderr = WritePipe::new_in_memory(); + let stdout = MemoryOutputPipe::new(); + let stderr = MemoryOutputPipe::new(); let r = { let mut linker = Linker::new(&ENGINE); add_to_linker(&mut linker)?; @@ -89,17 +89,11 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { }; r.map_err(move |trap: anyhow::Error| { - let stdout = stdout - .try_into_inner() - .expect("sole ref to stdout") - .into_inner(); + let stdout = stdout.finalize(); if !stdout.is_empty() { println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); } - let stderr = stderr - .try_into_inner() - .expect("sole ref to stderr") - .into_inner(); + let stderr = stderr.finalize(); if !stderr.is_empty() { println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); } @@ -256,8 +250,6 @@ async fn poll_oneoff_files() { run("poll_oneoff_files", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] -// This is a known bug with the preview 2 implementation: -#[should_panic] async fn poll_oneoff_stdio() { run("poll_oneoff_stdio", true).await.unwrap() } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index e5425f32431c..9286b378dff2 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -41,6 +41,7 @@ impl InnerInputPipe { } } +#[derive(Clone)] pub struct InputPipe(Arc>); #[async_trait::async_trait] @@ -76,6 +77,7 @@ struct InnerWrappedRead { reader: T, } +#[derive(Clone)] pub struct WrappedRead(Arc>>); impl WrappedRead { @@ -207,6 +209,7 @@ impl InnerOutputPipe { } } +#[derive(Clone)] pub struct OutputPipe(Arc>); #[async_trait::async_trait] @@ -265,6 +268,7 @@ struct InnerWrappedWrite { writer: T, } +#[derive(Clone)] pub struct WrappedWrite(Arc>>); impl WrappedWrite { @@ -307,3 +311,40 @@ impl HostOutputStream }) } } + +#[derive(Debug)] +struct InnerMemoryOutputPipe { + buffer: Vec, +} + +#[derive(Clone)] +pub struct MemoryOutputPipe(Arc>); + +impl MemoryOutputPipe { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(InnerMemoryOutputPipe { + buffer: Vec::new(), + }))) + } + + pub fn finalize(self) -> Vec { + Arc::try_unwrap(self.0) + .expect("finalizing MemoryOutputPipe") + .into_inner() + .buffer + } +} + +#[async_trait::async_trait] +impl HostOutputStream for MemoryOutputPipe { + async fn write(&mut self, buf: &[u8]) -> Result { + let mut i = self.0.lock().await; + i.buffer.extend(buf); + Ok(buf.len() as u64) + } + + fn pollable(&self) -> HostPollable { + // This stream is always ready for writing. + HostPollable::new(|| Box::pin(async { Ok(()) })) + } +} diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index aa652df8560c..56f0d4d4ca41 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,7 +1,7 @@ use anyhow::Error; -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; use crate::preview2::pipe::{WrappedRead, WrappedWrite}; +use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; // TODO: different cfg for windows here pub type Stdin = WrappedRead; From d11c28695ac1fb502b4dd622bf869b29f0bc9e75 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 22 Jun 2023 10:40:56 -0700 Subject: [PATCH 025/118] Remove todo --- crates/wasi/src/preview2/stream.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 5d03e3fe1c02..25264498d829 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,11 +1,6 @@ use crate::preview2::{HostPollable, Table, TableError}; use anyhow::Error; -// TODO(elliottt): wrapper of (Box, buffering), then impl HostInputStream -// for that type, then do the same for a dyn AsyncWrite with buffering for HostOutputStream. -// Testing could be to send the guest a timestamp and have them check that it's 100ms in the past? -// Try putting this in the command tests. - #[derive(Clone, Copy, Debug, PartialEq)] pub enum StreamState { Open, From ac71a90e5d74d85ef646a91214b9cadc19155df5 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 22 Jun 2023 17:31:19 -0700 Subject: [PATCH 026/118] rewrite nearly everything --- crates/wasi/src/preview2/filesystem.rs | 28 ++-- crates/wasi/src/preview2/pipe.rs | 174 +++++++++----------- crates/wasi/src/preview2/poll.rs | 87 +++++++--- crates/wasi/src/preview2/preview2/clocks.rs | 8 +- crates/wasi/src/preview2/preview2/io.rs | 80 +++++++-- crates/wasi/src/preview2/stdio.rs | 14 +- crates/wasi/src/preview2/stream.rs | 38 ++--- crates/wasi/src/preview2/table.rs | 2 +- 8 files changed, 248 insertions(+), 183 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 9fd3b7293ed9..ac2e0054eb93 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,6 +1,4 @@ -use crate::preview2::{ - HostInputStream, HostOutputStream, HostPollable, StreamState, Table, TableError, -}; +use crate::preview2::{HostInputStream, HostOutputStream, StreamState, Table, TableError}; use std::sync::Arc; bitflags::bitflags! { @@ -98,13 +96,13 @@ impl FileInputStream { #[async_trait::async_trait] impl HostInputStream for FileInputStream { - async fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { + fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; let (n, end) = read_result(self.file.read_at(buf, self.position))?; self.position = self.position.wrapping_add(n); Ok((n, end)) } - async fn read_vectored<'a>( + fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], ) -> anyhow::Result<(u64, StreamState)> { @@ -117,8 +115,8 @@ impl HostInputStream for FileInputStream { use system_interface::fs::FileIoExt; self.file.is_read_vectored_at() } - fn pollable(&self) -> HostPollable { - HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file reads cannot block + async fn ready(&mut self) -> anyhow::Result<()> { + Ok(()) // Always immediately ready - file reads cannot block } } @@ -146,7 +144,7 @@ impl FileOutputStream { #[async_trait::async_trait] impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. - async fn write(&mut self, buf: &[u8]) -> anyhow::Result { + fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; let n = self.file.write_at(buf, self.position)? as i64 as u64; self.position = self.position.wrapping_add(n); @@ -154,7 +152,7 @@ impl HostOutputStream for FileOutputStream { } /// Vectored-I/O form of `write`. - async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { + fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; let n = self.file.write_vectored_at(bufs, self.position)? as i64 as u64; self.position = self.position.wrapping_add(n); @@ -168,8 +166,8 @@ impl HostOutputStream for FileOutputStream { self.file.is_write_vectored_at() } - fn pollable(&self) -> HostPollable { - HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file writes cannot block + async fn ready(&mut self) -> anyhow::Result<()> { + Ok(()) // Always immediately ready - file writes cannot block } } @@ -185,13 +183,13 @@ impl FileAppendStream { #[async_trait::async_trait] impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. - async fn write(&mut self, buf: &[u8]) -> anyhow::Result { + fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; Ok(self.file.append(buf)? as i64 as u64) } /// Vectored-I/O form of `write`. - async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { + fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; let n = self.file.append_vectored(bufs)? as i64 as u64; Ok(n) @@ -204,7 +202,7 @@ impl HostOutputStream for FileAppendStream { self.file.is_write_vectored_at() } - fn pollable(&self) -> HostPollable { - HostPollable::new(|| Box::pin(async { Ok(()) })) // Always immediately ready - file appends cannot block + async fn ready(&mut self) -> anyhow::Result<()> { + Ok(()) // Always immediately ready - file appends cannot block } } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 9286b378dff2..6077d68b7c3c 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -7,31 +7,22 @@ //! Some convenience constructors are included for common backing types like `Vec` and `String`, //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; +use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; use anyhow::{anyhow, Error}; -use std::sync::Arc; -use tokio::sync::Mutex; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); - ( - InputPipe(Arc::new(tokio::sync::Mutex::new(InnerInputPipe::new( - reader, - )))), - OutputPipe(Arc::new(tokio::sync::Mutex::new(InnerOutputPipe::new( - writer, - )))), - ) + (InputPipe::new(reader), OutputPipe::new(writer)) } -struct InnerInputPipe { +pub struct InputPipe { state: StreamState, buffer: Vec, channel: tokio::sync::mpsc::Receiver>, } -impl InnerInputPipe { +impl InputPipe { fn new(channel: tokio::sync::mpsc::Receiver>) -> Self { Self { state: StreamState::Open, @@ -41,20 +32,17 @@ impl InnerInputPipe { } } -#[derive(Clone)] -pub struct InputPipe(Arc>); - #[async_trait::async_trait] impl HostInputStream for InputPipe { - async fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - let mut i = self.0.lock().await; - let l = i.buffer.len().min(dest.len()); + fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + let l = self.buffer.len().min(dest.len()); let dest = &mut dest[..l]; - dest.copy_from_slice(&i.buffer[..l]); - i.buffer = i.buffer.split_off(l); - Ok((l as u64, i.state)) + dest.copy_from_slice(&self.buffer[..l]); + self.buffer = self.buffer.split_off(l); + Ok((l as u64, self.state)) } + /* fn pollable(&self) -> HostPollable { let i = Arc::clone(&self.0); HostPollable::new(move || { @@ -69,49 +57,49 @@ impl HostInputStream for InputPipe { }) }) } + */ + async fn ready(&mut self) -> Result<(), Error> { + todo!() // FIXME + } } -struct InnerWrappedRead { +pub struct WrappedRead { state: StreamState, buffer: Vec, reader: T, } -#[derive(Clone)] -pub struct WrappedRead(Arc>>); - impl WrappedRead { pub fn new(reader: T) -> Self { - Self(Arc::new(Mutex::new(InnerWrappedRead { + WrappedRead { state: StreamState::Open, buffer: Vec::new(), reader, - }))) + } } } #[async_trait::async_trait] impl HostInputStream for WrappedRead { - async fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { use std::io::Write; - use tokio::io::AsyncReadExt; - let mut i = self.0.lock().await; - let l = dest.write(&i.buffer)?; + let l = dest.write(&self.buffer)?; - i.buffer.drain(..l); - if !i.buffer.is_empty() { + self.buffer.drain(..l); + if !self.buffer.is_empty() { return Ok((l as u64, StreamState::Open)); } - if i.state.is_closed() { + if self.state.is_closed() { return Ok((l as u64, StreamState::Closed)); } let mut dest = &mut dest[l..]; let rest = if !dest.is_empty() { - let written = i.reader.read_buf(&mut dest).await?; + let written = todo!() /* self.reader.read_buf(&mut dest).await? */ ; // FIXME we want to poll_read + // here if written == 0 { - i.state = StreamState::Closed; + self.state = StreamState::Closed; } written } else { @@ -120,9 +108,10 @@ impl HostInputStream fo // TODO: figure out how we're tracking the stream state. Maybe mutate it when handling the // result of `read_buf`? - Ok(((l + rest) as u64, i.state)) + Ok(((l + rest) as u64, self.state)) } + /* fn pollable(&self) -> HostPollable { use tokio::io::AsyncReadExt; let i = Arc::clone(&self.0); @@ -154,6 +143,10 @@ impl HostInputStream fo }) }) } + */ + async fn ready(&mut self) -> Result<(), Error> { + todo!() + } } enum SenderState { @@ -161,12 +154,12 @@ enum SenderState { Channel(tokio::sync::mpsc::Sender>), } -struct InnerOutputPipe { +pub struct OutputPipe { buffer: Vec, channel: Option, } -impl InnerOutputPipe { +impl OutputPipe { fn new(s: tokio::sync::mpsc::Sender>) -> Self { Self { buffer: Vec::new(), @@ -209,18 +202,14 @@ impl InnerOutputPipe { } } -#[derive(Clone)] -pub struct OutputPipe(Arc>); - #[async_trait::async_trait] impl HostOutputStream for OutputPipe { - async fn write(&mut self, buf: &[u8]) -> Result { + fn write(&mut self, buf: &[u8]) -> Result { use tokio::sync::mpsc::error::TrySendError; - let mut i = self.0.lock().await; - let mut bytes = core::mem::take(&mut i.buffer); + let mut bytes = core::mem::take(&mut self.buffer); bytes.extend(buf); - let (s, bytes) = match i.take_channel() { + let (s, bytes) = match self.take_channel() { SenderState::Writable(p) => { let s = p.send(bytes); (s, Vec::new()) @@ -237,12 +226,13 @@ impl HostOutputStream for OutputPipe { }, }; - i.buffer = bytes; - i.channel = Some(SenderState::Channel(s)); + self.buffer = bytes; + self.channel = Some(SenderState::Channel(s)); Ok(buf.len() as u64) } + /* fn pollable(&self) -> HostPollable { let i = Arc::clone(&self.0); HostPollable::new(move || { @@ -261,22 +251,23 @@ impl HostOutputStream for OutputPipe { }) }) } + */ + async fn ready(&mut self) -> Result<(), Error> { + todo!() // FIXME + } } -struct InnerWrappedWrite { +pub struct WrappedWrite { buffer: Vec, writer: T, } -#[derive(Clone)] -pub struct WrappedWrite(Arc>>); - impl WrappedWrite { pub fn new(writer: T) -> Self { - WrappedWrite(Arc::new(Mutex::new(InnerWrappedWrite { + WrappedWrite { buffer: Vec::new(), writer, - }))) + } } } @@ -284,67 +275,60 @@ impl WrappedWrite { impl HostOutputStream for WrappedWrite { - async fn write(&mut self, buf: &[u8]) -> Result { - use tokio::io::AsyncWriteExt; - let mut i = self.0.lock().await; - let mut bytes = core::mem::take(&mut i.buffer); + // I can get rid of the `async` here once the lock is no longer a tokio lock: + fn write(&mut self, buf: &[u8]) -> Result { + use std::pin::Pin; + use std::task::{Context, Poll}; + let mut bytes = core::mem::take(&mut self.buffer); bytes.extend(buf); - let written = i.writer.write_buf(&mut bytes.as_slice()).await?; - bytes.drain(..written); - i.buffer = bytes; - Ok(written as u64) + + // FIXME: either Waker::noop is stable, or its something trivial to implement, + // and we can use it here. + let mut no_op_context: Context<'_> = todo!(); + // This is a true non-blocking call to the writer + match Pin::new(&mut self.writer).poll_write(&mut no_op_context, &mut bytes.as_slice()) { + Poll::Pending => { + // Nothing was written: buffer all of it below. + } + Poll::Ready(written) => { + // So much was written: + bytes.drain(..written?); + } + } + self.buffer = bytes; + Ok(buf.len() as u64) } - fn pollable(&self) -> HostPollable { + async fn ready(&mut self) -> Result<(), Error> { use tokio::io::AsyncWriteExt; - let i = Arc::clone(&self.0); - HostPollable::new(move || { - let i = Arc::clone(&i); - Box::pin(async move { - let mut i = i.lock().await; - let bytes = core::mem::take(&mut i.buffer); - if !bytes.is_empty() { - i.writer.write_all(bytes.as_slice()).await?; - } - Ok(()) - }) - }) + let bytes = core::mem::take(&mut self.buffer); + if !bytes.is_empty() { + self.writer.write_all(bytes.as_slice()).await?; + } + Ok(()) } } #[derive(Debug)] -struct InnerMemoryOutputPipe { +struct MemoryOutputPipe { buffer: Vec, } -#[derive(Clone)] -pub struct MemoryOutputPipe(Arc>); - impl MemoryOutputPipe { pub fn new() -> Self { - Self(Arc::new(Mutex::new(InnerMemoryOutputPipe { - buffer: Vec::new(), - }))) - } - - pub fn finalize(self) -> Vec { - Arc::try_unwrap(self.0) - .expect("finalizing MemoryOutputPipe") - .into_inner() - .buffer + MemoryOutputPipe { buffer: Vec::new() } } } #[async_trait::async_trait] impl HostOutputStream for MemoryOutputPipe { - async fn write(&mut self, buf: &[u8]) -> Result { - let mut i = self.0.lock().await; - i.buffer.extend(buf); + fn write(&mut self, buf: &[u8]) -> Result { + self.buffer.extend(buf); Ok(buf.len() as u64) } - fn pollable(&self) -> HostPollable { + async fn ready(&mut self) -> Result<(), Error> { // This stream is always ready for writing. - HostPollable::new(|| Box::pin(async { Ok(()) })) + Ok(()) } } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index ce903f260fd7..3041d1ce07e1 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -3,18 +3,19 @@ use crate::preview2::{ Table, TableError, WasiView, }; use anyhow::{anyhow, Result}; +use std::any::Any; +use std::collections::{hash_map::Entry, HashMap}; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -type ReadynessFuture = Pin> + Send + Sync>>; +pub type PollableFuture<'a> = Pin> + Send + 'a>>; +pub type MakeFuture = for<'a> fn(&'a mut dyn Any) -> PollableFuture<'a>; +pub type ClosureFuture = Box PollableFuture<'static> + Send + Sync + 'static>; -pub struct HostPollable(Box ReadynessFuture + Send + Sync>); - -impl HostPollable { - pub fn new(mkfuture: impl Fn() -> ReadynessFuture + Send + Sync + 'static) -> HostPollable { - HostPollable(Box::new(mkfuture)) - } +pub enum HostPollable { + TableEntry { index: u32, make_future: MakeFuture }, + Closure(ClosureFuture), } pub trait TablePollableExt { @@ -43,27 +44,68 @@ impl poll::Host for T { } async fn poll_oneoff(&mut self, pollables: Vec) -> Result> { - struct PollOneoff { - elems: Vec<(u32, ReadynessFuture)>, + type ReadylistIndex = usize; + + let mut table = self.table_mut(); + + let mut table_futures: HashMap)> = HashMap::new(); + let mut closure_futures: Vec<(PollableFuture<'_>, Vec)> = Vec::new(); + + for (ix, p) in pollables.iter().enumerate() { + match table.get_host_pollable_mut(*p)? { + HostPollable::Closure(f) => closure_futures.push((f(), vec![ix])), + HostPollable::TableEntry { index, make_future } => { + match table_futures.entry(*index) { + Entry::Vacant(v) => { + v.insert((*make_future, vec![ix])); + } + Entry::Occupied(mut o) => { + let (_, v) = o.get_mut(); + v.push(ix); + } + } + } + } + } + + for (table_ix, (make_future, readylist_indicies)) in table_futures { + let a_mut = unsafe { + let a = table + .map + .get(&table_ix) + .ok_or(TableError::NotPresent)? + .as_ref(); + // This is safe because we have made sure that table_ix is + // unique, so we are only ever going to have one mut ref to + // this table element. + // We also are not going to use the table for anything else + // for the lifetime of these mut references. + #[allow(mutable_transmutes)] + std::mem::transmute::<&dyn Any, &mut dyn Any>(a) + }; + closure_futures.push((make_future(a_mut), readylist_indicies)); + } + + struct PollOneoff<'a> { + elems: Vec<(PollableFuture<'a>, Vec)>, } - impl Future for PollOneoff { + impl<'a> Future for PollOneoff<'a> { type Output = Result>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut any_ready = false; let mut results = vec![0; self.elems.len()]; - for (ix, (pollable, f)) in self.elems.iter_mut().enumerate() { - // NOTE: we don't need to guard against polling any of the elems more than - // once, as a single one becoming ready will cause the whole PollOneoff future - // to become ready. - match f.as_mut().poll(cx) { + for (fut, readylist_indicies) in self.elems.iter_mut() { + match fut.as_mut().poll(cx) { Poll::Ready(Ok(())) => { - results[ix] = 1; + for r in readylist_indicies { + results[*r] = 1; + } any_ready = true; } Poll::Ready(Err(e)) => { return Poll::Ready(Err( - e.context(format!("poll_oneoff[{ix}]: {pollable}")) + e.context(format!("poll_oneoff {readylist_indicies:?}")) )); } Poll::Pending => {} @@ -78,16 +120,7 @@ impl poll::Host for T { } Ok(PollOneoff { - elems: pollables - .iter() - .enumerate() - .map( - |(ix, pollable)| match self.table_mut().get_host_pollable_mut(*pollable) { - Ok(mkf) => Ok((*pollable, mkf.0())), - Err(e) => Err(anyhow!(e).context(format!("poll_oneoff[{ix}]: {pollable}"))), - }, - ) - .collect::>>()?, + elems: closure_futures, } .await?) } diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index 4816814ddaea..a2aa059c6d7b 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -59,7 +59,9 @@ impl monotonic_clock::Host for T { // Deadline is in the past, so pollable is always ready: Ok(self .table_mut() - .push_host_pollable(HostPollable::new(|| Box::pin(async { Ok(()) })))?) + .push_host_pollable(HostPollable::Closure(Box::new(|| { + Box::pin(async { Ok(()) }) + })))?) } else { let duration = if absolute { Duration::from_nanos(clock_now - when) @@ -76,7 +78,7 @@ impl monotonic_clock::Host for T { ); Ok(self .table_mut() - .push_host_pollable(HostPollable::new(move || { + .push_host_pollable(HostPollable::Closure(Box::new(move || { Box::pin(async move { tracing::trace!( "mkf: deadline = {:?}, now = {:?}", @@ -85,7 +87,7 @@ impl monotonic_clock::Host for T { ); Ok(tokio::time::sleep_until(deadline).await) }) - }))?) + })))?) } } } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index b213d1f8b242..708439a6f0ed 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -1,10 +1,12 @@ use crate::preview2::{ + poll::PollableFuture, stream::{HostInputStream, HostOutputStream, TableStreamExt}, wasi::io::streams::{self, InputStream, OutputStream, StreamError}, wasi::poll::poll::Pollable, - TableError, TablePollableExt, WasiView, + HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; +use std::any::Any; impl From for streams::Error { fn from(error: anyhow::Error) -> streams::Error { @@ -54,7 +56,7 @@ impl streams::Host for T { let buffer_len = std::cmp::min(len, 0x400000) as _; let mut buffer = vec![0; buffer_len]; - let (bytes_read, state) = s.read(&mut buffer).await?; + let (bytes_read, state) = HostInputStream::read(s.as_mut(), &mut buffer)?; buffer.truncate(bytes_read as usize); @@ -66,14 +68,17 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, bool), streams::Error> { - // TODO: When this is really async make this block. + self.table_mut() + .get_input_stream_mut(stream)? + .ready() + .await?; self.read(stream, len).await } async fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { let s = self.table_mut().get_output_stream_mut(stream)?; - let bytes_written: u64 = s.write(&bytes).await?; + let bytes_written: u64 = HostOutputStream::write(s.as_mut(), &bytes)?; Ok(u64::try_from(bytes_written).unwrap()) } @@ -83,14 +88,18 @@ impl streams::Host for T { stream: OutputStream, bytes: Vec, ) -> Result { - // TODO: When this is really async make this block. - self.write(stream, bytes).await + let written = self.write(stream, bytes).await?; + self.table_mut() + .get_output_stream_mut(stream)? + .ready() + .await?; + Ok(written) } async fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { let s = self.table_mut().get_input_stream_mut(stream)?; - let (bytes_skipped, end) = s.skip(len).await?; + let (bytes_skipped, end) = s.skip(len)?; Ok((bytes_skipped, end.is_closed())) } @@ -100,8 +109,12 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(u64, bool), streams::Error> { - // TODO: When this is really async make this block. - self.skip(stream, len).await + let r = self.skip(stream, len).await?; + self.table_mut() + .get_input_stream_mut(stream)? + .ready() + .await?; + Ok(r) } async fn write_zeroes( @@ -111,7 +124,7 @@ impl streams::Host for T { ) -> Result { let s = self.table_mut().get_output_stream_mut(stream)?; - let bytes_written: u64 = s.write_zeroes(len).await?; + let bytes_written: u64 = s.write_zeroes(len)?; Ok(bytes_written) } @@ -121,8 +134,12 @@ impl streams::Host for T { stream: OutputStream, len: u64, ) -> Result { - // TODO: When this is really async make this block. - self.write_zeroes(stream, len).await + let r = self.write_zeroes(stream, len).await?; + self.table_mut() + .get_output_stream_mut(stream)? + .ready() + .await?; + Ok(r) } async fn splice( @@ -160,8 +177,10 @@ impl streams::Host for T { dst: OutputStream, len: u64, ) -> Result<(u64, bool), streams::Error> { - // TODO: When this is really async make this block. - self.splice(src, dst, len).await + // TODO: once splice is implemented, figure out what the blocking semantics are for waiting + // on src and dest here. probably just delete these blocking_ funcs altogether and defer + // that decision to userland, though. + todo!("stream splice is not implemented") } async fn forward( @@ -194,7 +213,21 @@ impl streams::Host for T { } async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { - let pollable = self.table().get_input_stream(stream)?.pollable(); + let _ = self.table().get_input_stream(stream)?; + + fn input_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + let stream = stream + .downcast_mut::>() + // Should be impossible because we made sure this will downcast to a + // HostImputStream with table check above. + .expect("downcast to HostInputStream failed"); + stream.ready() + } + + let pollable = HostPollable::TableEntry { + index: stream, + make_future: input_stream_ready, + }; Ok(self.table_mut().push_host_pollable(pollable)?) } @@ -202,7 +235,22 @@ impl streams::Host for T { &mut self, stream: OutputStream, ) -> anyhow::Result { - let pollable = self.table().get_output_stream(stream)?.pollable(); + let _ = self.table().get_output_stream(stream)?; + + fn output_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + let stream = stream + .downcast_mut::>() + // Should be impossible because we made sure this will downcast to a + // HostOutputStream with table check above. + .expect("downcast to HostOutputStream failed"); + stream.ready() + } + + let pollable = HostPollable::TableEntry { + index: stream, + make_future: output_stream_ready, + }; + Ok(self.table_mut().push_host_pollable(pollable)?) } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 56f0d4d4ca41..18c5301d25d5 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,7 +1,7 @@ use anyhow::Error; use crate::preview2::pipe::{WrappedRead, WrappedWrite}; -use crate::preview2::{HostInputStream, HostOutputStream, HostPollable, StreamState}; +use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; // TODO: different cfg for windows here pub type Stdin = WrappedRead; @@ -25,11 +25,11 @@ pub struct EmptyStream; #[async_trait::async_trait] impl HostInputStream for EmptyStream { - async fn read(&mut self, _buf: &mut [u8]) -> Result<(u64, StreamState), Error> { + fn read(&mut self, _buf: &mut [u8]) -> Result<(u64, StreamState), Error> { Ok((0, StreamState::Open)) } - fn pollable(&self) -> HostPollable { + async fn ready(&mut self) -> Result<(), Error> { struct Never; impl std::future::Future for Never { @@ -43,18 +43,18 @@ impl HostInputStream for EmptyStream { } // This stream is never ready for reading. - HostPollable::new(|| Box::pin(Never)) + Never.await } } #[async_trait::async_trait] impl HostOutputStream for EmptyStream { - async fn write(&mut self, buf: &[u8]) -> Result { + fn write(&mut self, buf: &[u8]) -> Result { Ok(buf.len() as u64) } - fn pollable(&self) -> HostPollable { + async fn ready(&mut self) -> Result<(), Error> { // This stream is always ready for writing. - HostPollable::new(|| Box::pin(async { Ok(()) })) + Ok(()) } } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 25264498d829..31148ac5d4b1 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,4 +1,4 @@ -use crate::preview2::{HostPollable, Table, TableError}; +use crate::preview2::{Table, TableError}; use anyhow::Error; #[derive(Clone, Copy, Debug, PartialEq)] @@ -22,17 +22,17 @@ impl StreamState { pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes read /// and a flag indicating whether the end of the stream was reached. - async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; + fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; /// Vectored-I/O form of `read`. - async fn read_vectored<'a>( + fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], ) -> Result<(u64, StreamState), Error> { if bufs.len() > 0 { - self.read(bufs.get_mut(0).unwrap()).await + self.read(bufs.get_mut(0).unwrap()) } else { - self.read(&mut []).await + self.read(&mut []) } } @@ -43,13 +43,13 @@ pub trait HostInputStream: Send + Sync { } /// Read bytes from a stream and discard them. - async fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { + fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let mut nread = 0; let mut state = StreamState::Open; // TODO: Optimize by reading more than one byte at a time. for _ in 0..nelem { - let (num, read_state) = self.read(&mut [0]).await?; + let (num, read_state) = self.read(&mut [0])?; nread += num; if read_state.is_closed() { state = read_state; @@ -60,8 +60,8 @@ pub trait HostInputStream: Send + Sync { Ok((nread, state)) } - /// Get the Pollable implementation for read readiness. - fn pollable(&self) -> HostPollable; + /// An async method to check read readiness. + async fn ready(&mut self) -> Result<(), Error>; } /// An output bytestream. @@ -72,12 +72,12 @@ pub trait HostInputStream: Send + Sync { #[async_trait::async_trait] pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. - async fn write(&mut self, _buf: &[u8]) -> Result; + fn write(&mut self, _buf: &[u8]) -> Result; /// Vectored-I/O form of `write`. - async fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { + fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { if bufs.len() > 0 { - self.write(bufs.get(0).unwrap()).await + self.write(bufs.get(0).unwrap()) } else { Ok(0) } @@ -90,7 +90,7 @@ pub trait HostOutputStream: Send + Sync { } /// Transfer bytes directly from an input stream to an output stream. - async fn splice( + fn splice( &mut self, src: &mut dyn HostInputStream, nelem: u64, @@ -101,8 +101,8 @@ pub trait HostOutputStream: Send + Sync { // TODO: Optimize by splicing more than one byte at a time. for _ in 0..nelem { let mut buf = [0u8]; - let (num, read_state) = src.read(&mut buf).await?; - self.write(&buf).await?; + let (num, read_state) = src.read(&mut buf)?; + self.write(&buf)?; nspliced += num; if read_state.is_closed() { state = read_state; @@ -114,12 +114,12 @@ pub trait HostOutputStream: Send + Sync { } /// Repeatedly write a byte to a stream. - async fn write_zeroes(&mut self, nelem: u64) -> Result { + fn write_zeroes(&mut self, nelem: u64) -> Result { let mut nwritten = 0; // TODO: Optimize by writing more than one byte at a time. for _ in 0..nelem { - let num = self.write(&[0]).await?; + let num = self.write(&[0])?; if num == 0 { break; } @@ -129,8 +129,8 @@ pub trait HostOutputStream: Send + Sync { Ok(nwritten) } - /// Get the Pollable implementation for write readiness. - fn pollable(&self) -> HostPollable; + /// An async method to check write readiness. + async fn ready(&mut self) -> Result<(), Error>; } pub trait TableStreamExt { diff --git a/crates/wasi/src/preview2/table.rs b/crates/wasi/src/preview2/table.rs index dd1017b751bd..9f1d16cb775d 100644 --- a/crates/wasi/src/preview2/table.rs +++ b/crates/wasi/src/preview2/table.rs @@ -20,7 +20,7 @@ pub enum TableError { /// up. Right now it is just an approximation. #[derive(Debug)] pub struct Table { - map: HashMap>, + pub(crate) map: HashMap>, next_key: u32, } From 35d8b6a8566df1a3f15cf76505c74bb10fd127c9 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 22 Jun 2023 18:07:47 -0700 Subject: [PATCH 027/118] implement the pipe stuff --- crates/wasi/src/preview2/pipe.rs | 190 +++++++++++++++++-------------- 1 file changed, 102 insertions(+), 88 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 6077d68b7c3c..c78ba48a1a8d 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -9,6 +9,8 @@ //! use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; use anyhow::{anyhow, Error}; +use std::pin::Pin; +use std::task::{Context, Poll, Waker}; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); @@ -35,31 +37,42 @@ impl InputPipe { #[async_trait::async_trait] impl HostInputStream for InputPipe { fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - let l = self.buffer.len().min(dest.len()); - let dest = &mut dest[..l]; - dest.copy_from_slice(&self.buffer[..l]); - self.buffer = self.buffer.split_off(l); - Ok((l as u64, self.state)) - } - - /* - fn pollable(&self) -> HostPollable { - let i = Arc::clone(&self.0); - HostPollable::new(move || { - let i = Arc::clone(&i); - Box::pin(async move { - let mut i = i.lock().await; - match i.channel.recv().await { - None => i.state = StreamState::Closed, - Some(mut buf) => i.buffer.append(&mut buf), + use tokio::sync::mpsc::error::TryRecvError; + let read_from_buffer = self.buffer.len().min(dest.len()); + let buffer_dest = &mut dest[..read_from_buffer]; + buffer_dest.copy_from_slice(&self.buffer[..read_from_buffer]); + // Keep remaining contents in buffer + self.buffer = self.buffer.split_off(read_from_buffer); + if read_from_buffer < dest.len() { + match self.channel.try_recv() { + Ok(msg) => { + let recv_dest = &mut dest[read_from_buffer..]; + if msg.len() < recv_dest.len() { + recv_dest[..msg.len()].copy_from_slice(&msg); + Ok(((read_from_buffer + msg.len()) as u64, self.state)) + } else { + recv_dest.copy_from_slice(&msg[..recv_dest.len()]); + self.buffer.extend_from_slice(&msg[recv_dest.len()..]); + Ok((dest.len() as u64, self.state)) + } } - Ok(()) - }) - }) + Err(TryRecvError::Empty) => Ok((read_from_buffer as u64, self.state)), + Err(TryRecvError::Disconnected) => { + self.state = StreamState::Closed; + Ok((read_from_buffer as u64, self.state)) + } + } + } else { + Ok((read_from_buffer as u64, self.state)) + } } - */ + async fn ready(&mut self) -> Result<(), Error> { - todo!() // FIXME + match self.channel.recv().await { + None => self.state = StreamState::Closed, + Some(mut buf) => self.buffer.append(&mut buf), + } + Ok(()) } } @@ -96,56 +109,51 @@ impl HostInputStream fo let mut dest = &mut dest[l..]; let rest = if !dest.is_empty() { - let written = todo!() /* self.reader.read_buf(&mut dest).await? */ ; // FIXME we want to poll_read - // here - if written == 0 { + let mut readbuf = tokio::io::ReadBuf::new(dest); + + let noop_waker = noop_waker(); + let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // Make a synchronous, non-blocking call attempt to read. We are not + // going to poll this more than once, so the noop waker is appropriate. + match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { + Poll::Pending => {} // Nothing was read + Poll::Ready(result) => result?, // Maybe an error occured + }; + let bytes_read = readbuf.filled().len(); + + if bytes_read == 0 { self.state = StreamState::Closed; } - written + bytes_read } else { 0 }; - // TODO: figure out how we're tracking the stream state. Maybe mutate it when handling the - // result of `read_buf`? Ok(((l + rest) as u64, self.state)) } - /* - fn pollable(&self) -> HostPollable { - use tokio::io::AsyncReadExt; - let i = Arc::clone(&self.0); - HostPollable::new(move || { - let i = Arc::clone(&i); - Box::pin(async move { - let mut i = i.lock().await; - - if i.state.is_closed() { - return Ok(()); - } - - let mut bytes = core::mem::take(&mut i.buffer); - let start = bytes.len(); - bytes.resize(start + 1024, 0); - let l = i.reader.read_buf(&mut &mut bytes[start..]).await?; + async fn ready(&mut self) -> Result<(), Error> { + if self.state.is_closed() { + return Ok(()); + } - // Reading 0 bytes means either there wasn't enough space in the buffer (which we - // know there is because we just resized) or that the stream has closed. Thus, we - // know the stream has closed here. - if l == 0 { - i.state = StreamState::Closed; - } + let mut bytes = core::mem::take(&mut self.buffer); + let start = bytes.len(); + bytes.resize(start + 1024, 0); + let l = + tokio::io::AsyncReadExt::read_buf(&mut self.reader, &mut &mut bytes[start..]).await?; + + // Reading 0 bytes means either there wasn't enough space in the buffer (which we + // know there is because we just resized) or that the stream has closed. Thus, we + // know the stream has closed here. + if l == 0 { + self.state = StreamState::Closed; + } - bytes.drain(start + l..); - i.buffer = bytes; + bytes.drain(start + l..); + self.buffer = bytes; - Ok(()) - }) - }) - } - */ - async fn ready(&mut self) -> Result<(), Error> { - todo!() + Ok(()) } } @@ -232,28 +240,16 @@ impl HostOutputStream for OutputPipe { Ok(buf.len() as u64) } - /* - fn pollable(&self) -> HostPollable { - let i = Arc::clone(&self.0); - HostPollable::new(move || { - let i = Arc::clone(&i); - Box::pin(async move { - let mut i = i.lock().await; - i.flush().await; - let p = match i.channel.take().expect("Missing sender channel state") { - SenderState::Writable(p) => p, - SenderState::Channel(s) => s.reserve_owned().await?, - }; - - i.channel = Some(SenderState::Writable(p)); - - Ok(()) - }) - }) - } - */ async fn ready(&mut self) -> Result<(), Error> { - todo!() // FIXME + self.flush().await; + let p = match self.channel.take().expect("Missing sender channel state") { + SenderState::Writable(p) => p, + SenderState::Channel(s) => s.reserve_owned().await?, + }; + + self.channel = Some(SenderState::Writable(p)); + + Ok(()) } } @@ -277,16 +273,14 @@ impl HostOutputStream { // I can get rid of the `async` here once the lock is no longer a tokio lock: fn write(&mut self, buf: &[u8]) -> Result { - use std::pin::Pin; - use std::task::{Context, Poll}; let mut bytes = core::mem::take(&mut self.buffer); bytes.extend(buf); - // FIXME: either Waker::noop is stable, or its something trivial to implement, - // and we can use it here. - let mut no_op_context: Context<'_> = todo!(); - // This is a true non-blocking call to the writer - match Pin::new(&mut self.writer).poll_write(&mut no_op_context, &mut bytes.as_slice()) { + let noop_waker = noop_waker(); + let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // Make a synchronous, non-blocking call attempt to write. We are not + // going to poll this more than once, so the noop waker is appropriate. + match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { Poll::Pending => { // Nothing was written: buffer all of it below. } @@ -332,3 +326,23 @@ impl HostOutputStream for MemoryOutputPipe { Ok(()) } } + +// This implementation is basically copy-pasted out of `std` because the +// implementation there has not yet stabilized. When the `noop_waker` feature +// stabilizes, replace this with std::task::Waker::noop(). +fn noop_waker() -> Waker { + use std::task::{RawWaker, RawWakerVTable}; + const VTABLE: RawWakerVTable = RawWakerVTable::new( + // Cloning just returns a new no-op raw waker + |_| RAW, + // `wake` does nothing + |_| {}, + // `wake_by_ref` does nothing + |_| {}, + // Dropping does nothing as we don't allocate anything + |_| {}, + ); + const RAW: RawWaker = RawWaker::new(std::ptr::null(), &VTABLE); + + unsafe { Waker::from_raw(RAW) } +} From a2326e7b244430fe4a3efaa4efa24e780275da92 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 22 Jun 2023 18:10:49 -0700 Subject: [PATCH 028/118] wibble --- crates/wasi/src/preview2/pipe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index c78ba48a1a8d..aee87d458c13 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -304,7 +304,7 @@ impl HostOutputStream } #[derive(Debug)] -struct MemoryOutputPipe { +pub struct MemoryOutputPipe { buffer: Vec, } From 621956f94d63d09cc8a900d5746fe07895a88fa7 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 22 Jun 2023 20:09:44 -0700 Subject: [PATCH 029/118] fix MemoryOutputPipe just enough to make the tests compile --- crates/wasi/src/preview2/pipe.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index aee87d458c13..2314933e8773 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -303,21 +303,31 @@ impl HostOutputStream } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MemoryOutputPipe { - buffer: Vec, + buffer: std::sync::Arc>>, } impl MemoryOutputPipe { pub fn new() -> Self { - MemoryOutputPipe { buffer: Vec::new() } + MemoryOutputPipe { + buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + } + } + pub fn finalize(self) -> Vec { + std::sync::Arc::try_unwrap(self.buffer) + .map_err(|_| ()) + .expect("more than one outstanding reference") + .into_inner() + .map_err(|_| ()) + .expect("mutex poisioned") } } #[async_trait::async_trait] impl HostOutputStream for MemoryOutputPipe { fn write(&mut self, buf: &[u8]) -> Result { - self.buffer.extend(buf); + self.buffer.lock().unwrap().extend(buf); Ok(buf.len() as u64) } From 2b793ef4e2dd25d2b2b064b694ae7e3ac82eb8b2 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 23 Jun 2023 16:06:42 -0700 Subject: [PATCH 030/118] Move the table iteration into a helper function --- crates/wasi/src/preview2/poll.rs | 19 +++---------------- crates/wasi/src/preview2/table.rs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 3041d1ce07e1..06239b735c59 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -68,22 +68,9 @@ impl poll::Host for T { } } - for (table_ix, (make_future, readylist_indicies)) in table_futures { - let a_mut = unsafe { - let a = table - .map - .get(&table_ix) - .ok_or(TableError::NotPresent)? - .as_ref(); - // This is safe because we have made sure that table_ix is - // unique, so we are only ever going to have one mut ref to - // this table element. - // We also are not going to use the table for anything else - // for the lifetime of these mut references. - #[allow(mutable_transmutes)] - std::mem::transmute::<&dyn Any, &mut dyn Any>(a) - }; - closure_futures.push((make_future(a_mut), readylist_indicies)); + for (entry, (make_future, readylist_indices)) in table.iter_entries(table_futures) { + let entry = entry?; + closure_futures.push((make_future(entry), readylist_indices)); } struct PollOneoff<'a> { diff --git a/crates/wasi/src/preview2/table.rs b/crates/wasi/src/preview2/table.rs index 9f1d16cb775d..79dbc6746bcd 100644 --- a/crates/wasi/src/preview2/table.rs +++ b/crates/wasi/src/preview2/table.rs @@ -104,4 +104,23 @@ impl Table { } } } + + /// Zip the values of the map with mutable references to table entries corresponding to each + /// key. As the keys in the [HashMap] are unique, this iterator can give mutable references + /// with the same lifetime as the mutable reference to the [Table]. + pub fn iter_entries<'a, T>( + &'a mut self, + map: HashMap, + ) -> impl Iterator, T)> { + map.into_iter().map(move |(k, v)| { + let item = self + .map + .get_mut(&k) + .map(Box::as_mut) + // Safety: extending the lifetime of the mutable reference. + .map(|item| unsafe { &mut *(item as *mut dyn Any) }) + .ok_or(TableError::NotPresent); + (item, v) + }) + } } From 1dedcf256810e19dc1f846ac9652da2cc784fb93 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 23 Jun 2023 16:47:14 -0700 Subject: [PATCH 031/118] AsyncFd stream implementation to fix stdin on unix --- crates/wasi/Cargo.toml | 2 +- crates/wasi/src/preview2/pipe.rs | 78 +++++++++++++++++++++++++++++++ crates/wasi/src/preview2/stdio.rs | 13 ++++-- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index dc5e017cc1f5..f0b63267fe1f 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -22,7 +22,7 @@ wiggle = { workspace = true, optional = true } libc = { workspace = true } once_cell = { workspace = true } -tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt"] } +tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "net"] } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 2314933e8773..2ade54bf7aae 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -76,6 +76,84 @@ impl HostInputStream for InputPipe { } } +#[cfg(unix)] +pub use async_fd_stream::*; + +#[cfg(unix)] +mod async_fd_stream { + use super::{HostInputStream, HostOutputStream, StreamState}; + use anyhow::Error; + use std::io::{Read, Write}; + use std::os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd}; + use tokio::io::unix::AsyncFd; + + pub struct AsyncFdStream { + fd: AsyncFd, + } + + impl AsyncFdStream { + pub fn new(fd: T) -> Self { + Self { + fd: AsyncFd::new(fd).unwrap(), + } + } + } + + #[async_trait::async_trait] + impl HostInputStream for AsyncFdStream { + fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // we're done. + let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + + // TODO: how sure are we that this is non-blocking? + let read_res = file.read(dest); + + // Make sure that the file doesn't close the fd when it's dropped. + file.into_raw_fd(); + + let n = read_res?; + + // TODO: figure out when the stream should be considered closed + // TODO: figure out how to handle the error conditions from the read call above + + Ok((n as u64, StreamState::Open)) + } + + async fn ready(&mut self) -> Result<(), Error> { + self.fd.readable().await?; + Ok(()) + } + } + + #[async_trait::async_trait] + impl HostOutputStream for AsyncFdStream { + fn write(&mut self, buf: &[u8]) -> Result { + // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // we're done. + let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + + // TODO: how sure are we that this is non-blocking? + let write_res = file.write(buf); + + // Make sure that the file doesn't close the fd when it's dropped. + file.into_raw_fd(); + + let n = write_res?; + + // TODO: figure out when the stream should be considered closed + // TODO: figure out how to handle the error conditions from the write call above + + Ok(n as u64) + } + + async fn ready(&mut self) -> Result<(), Error> { + self.fd.writable().await?; + Ok(()) + } + } +} + pub struct WrappedRead { state: StreamState, buffer: Vec, diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 18c5301d25d5..58c50874fd23 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -3,11 +3,18 @@ use anyhow::Error; use crate::preview2::pipe::{WrappedRead, WrappedWrite}; use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; +pub use self::stdin::*; + // TODO: different cfg for windows here -pub type Stdin = WrappedRead; +#[cfg(unix)] +mod stdin { + use crate::preview2::pipe::{AsyncFdStream, WrappedRead, WrappedWrite}; + + pub type Stdin = AsyncFdStream; -pub fn stdin() -> Stdin { - WrappedRead::new(tokio::io::stdin()) + pub fn stdin() -> Stdin { + AsyncFdStream::new(std::io::stdin()) + } } pub type Stdout = WrappedWrite; From 8001a7a1f4c169e21c352b1bffb674c5a5262816 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 23 Jun 2023 16:49:58 -0700 Subject: [PATCH 032/118] Rename Wrapped{Read,Write} streams to Async{Read,Write}Stream --- crates/wasi/src/preview2/pipe.rs | 16 ++++++++-------- crates/wasi/src/preview2/stdio.rs | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 2ade54bf7aae..ae15f0530d6d 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -154,15 +154,15 @@ mod async_fd_stream { } } -pub struct WrappedRead { +pub struct AsyncReadStream { state: StreamState, buffer: Vec, reader: T, } -impl WrappedRead { +impl AsyncReadStream { pub fn new(reader: T) -> Self { - WrappedRead { + AsyncReadStream { state: StreamState::Open, buffer: Vec::new(), reader, @@ -171,7 +171,7 @@ impl WrappedRead { } #[async_trait::async_trait] -impl HostInputStream for WrappedRead { +impl HostInputStream for AsyncReadStream { fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { use std::io::Write; let l = dest.write(&self.buffer)?; @@ -331,14 +331,14 @@ impl HostOutputStream for OutputPipe { } } -pub struct WrappedWrite { +pub struct AsyncWriteStream { buffer: Vec, writer: T, } -impl WrappedWrite { +impl AsyncWriteStream { pub fn new(writer: T) -> Self { - WrappedWrite { + AsyncWriteStream { buffer: Vec::new(), writer, } @@ -347,7 +347,7 @@ impl WrappedWrite { #[async_trait::async_trait] impl HostOutputStream - for WrappedWrite + for AsyncWriteStream { // I can get rid of the `async` here once the lock is no longer a tokio lock: fn write(&mut self, buf: &[u8]) -> Result { diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 58c50874fd23..85719570f3ec 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,6 +1,6 @@ use anyhow::Error; -use crate::preview2::pipe::{WrappedRead, WrappedWrite}; +use crate::preview2::pipe::{AsyncReadStream, AsyncWriteStream}; use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; pub use self::stdin::*; @@ -8,7 +8,7 @@ pub use self::stdin::*; // TODO: different cfg for windows here #[cfg(unix)] mod stdin { - use crate::preview2::pipe::{AsyncFdStream, WrappedRead, WrappedWrite}; + use crate::preview2::pipe::{AsyncReadStream, AsyncWriteStream, AsyncFdStream}; pub type Stdin = AsyncFdStream; @@ -17,15 +17,15 @@ mod stdin { } } -pub type Stdout = WrappedWrite; +pub type Stdout = AsyncWriteStream; pub fn stdout() -> Stdout { - WrappedWrite::new(tokio::io::stdout()) + AsyncWriteStream::new(tokio::io::stdout()) } -pub type Stderr = WrappedWrite; +pub type Stderr = AsyncWriteStream; pub fn stderr() -> Stderr { - WrappedWrite::new(tokio::io::stderr()) + AsyncWriteStream::new(tokio::io::stderr()) } pub struct EmptyStream; From 31f4c6b26266f0fb80844d3c4a2a2c4c295b6fe9 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 23 Jun 2023 16:58:38 -0700 Subject: [PATCH 033/118] Move async io wrappers into stream.rs --- crates/wasi/src/preview2/mod.rs | 7 +- crates/wasi/src/preview2/pipe.rs | 235 +----------------------- crates/wasi/src/preview2/poll.rs | 4 +- crates/wasi/src/preview2/preview2/io.rs | 6 +- crates/wasi/src/preview2/stdio.rs | 5 +- crates/wasi/src/preview2/stream.rs | 233 +++++++++++++++++++++++ 6 files changed, 248 insertions(+), 242 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 571e24fc156a..706907e3ce9c 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -39,7 +39,12 @@ pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; -pub use self::stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}; +#[cfg(unix)] +pub use self::stream::AsyncFdStream; +pub use self::stream::{ + AsyncReadStream, AsyncWriteStream, HostInputStream, HostOutputStream, StreamState, + TableStreamExt, +}; pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index ae15f0530d6d..35b3e4480586 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -8,9 +8,7 @@ //! but the virtual pipes can be instantiated with any `Read` or `Write` type. //! use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; -use anyhow::{anyhow, Error}; -use std::pin::Pin; -use std::task::{Context, Poll, Waker}; +use anyhow::Error; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); @@ -76,165 +74,6 @@ impl HostInputStream for InputPipe { } } -#[cfg(unix)] -pub use async_fd_stream::*; - -#[cfg(unix)] -mod async_fd_stream { - use super::{HostInputStream, HostOutputStream, StreamState}; - use anyhow::Error; - use std::io::{Read, Write}; - use std::os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd}; - use tokio::io::unix::AsyncFd; - - pub struct AsyncFdStream { - fd: AsyncFd, - } - - impl AsyncFdStream { - pub fn new(fd: T) -> Self { - Self { - fd: AsyncFd::new(fd).unwrap(), - } - } - } - - #[async_trait::async_trait] - impl HostInputStream for AsyncFdStream { - fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // we're done. - let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - - // TODO: how sure are we that this is non-blocking? - let read_res = file.read(dest); - - // Make sure that the file doesn't close the fd when it's dropped. - file.into_raw_fd(); - - let n = read_res?; - - // TODO: figure out when the stream should be considered closed - // TODO: figure out how to handle the error conditions from the read call above - - Ok((n as u64, StreamState::Open)) - } - - async fn ready(&mut self) -> Result<(), Error> { - self.fd.readable().await?; - Ok(()) - } - } - - #[async_trait::async_trait] - impl HostOutputStream for AsyncFdStream { - fn write(&mut self, buf: &[u8]) -> Result { - // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // we're done. - let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - - // TODO: how sure are we that this is non-blocking? - let write_res = file.write(buf); - - // Make sure that the file doesn't close the fd when it's dropped. - file.into_raw_fd(); - - let n = write_res?; - - // TODO: figure out when the stream should be considered closed - // TODO: figure out how to handle the error conditions from the write call above - - Ok(n as u64) - } - - async fn ready(&mut self) -> Result<(), Error> { - self.fd.writable().await?; - Ok(()) - } - } -} - -pub struct AsyncReadStream { - state: StreamState, - buffer: Vec, - reader: T, -} - -impl AsyncReadStream { - pub fn new(reader: T) -> Self { - AsyncReadStream { - state: StreamState::Open, - buffer: Vec::new(), - reader, - } - } -} - -#[async_trait::async_trait] -impl HostInputStream for AsyncReadStream { - fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - use std::io::Write; - let l = dest.write(&self.buffer)?; - - self.buffer.drain(..l); - if !self.buffer.is_empty() { - return Ok((l as u64, StreamState::Open)); - } - - if self.state.is_closed() { - return Ok((l as u64, StreamState::Closed)); - } - - let mut dest = &mut dest[l..]; - let rest = if !dest.is_empty() { - let mut readbuf = tokio::io::ReadBuf::new(dest); - - let noop_waker = noop_waker(); - let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // Make a synchronous, non-blocking call attempt to read. We are not - // going to poll this more than once, so the noop waker is appropriate. - match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { - Poll::Pending => {} // Nothing was read - Poll::Ready(result) => result?, // Maybe an error occured - }; - let bytes_read = readbuf.filled().len(); - - if bytes_read == 0 { - self.state = StreamState::Closed; - } - bytes_read - } else { - 0 - }; - - Ok(((l + rest) as u64, self.state)) - } - - async fn ready(&mut self) -> Result<(), Error> { - if self.state.is_closed() { - return Ok(()); - } - - let mut bytes = core::mem::take(&mut self.buffer); - let start = bytes.len(); - bytes.resize(start + 1024, 0); - let l = - tokio::io::AsyncReadExt::read_buf(&mut self.reader, &mut &mut bytes[start..]).await?; - - // Reading 0 bytes means either there wasn't enough space in the buffer (which we - // know there is because we just resized) or that the stream has closed. Thus, we - // know the stream has closed here. - if l == 0 { - self.state = StreamState::Closed; - } - - bytes.drain(start + l..); - self.buffer = bytes; - - Ok(()) - } -} - enum SenderState { Writable(tokio::sync::mpsc::OwnedPermit>), Channel(tokio::sync::mpsc::Sender>), @@ -307,7 +146,7 @@ impl HostOutputStream for OutputPipe { Err(TrySendError::Closed(_)) => { // TODO: we may need to communicate failure out in a way that doesn't result in // a trap. - return Err(anyhow!("pipe closed")); + return Err(anyhow::anyhow!("pipe closed")); } }, }; @@ -331,56 +170,6 @@ impl HostOutputStream for OutputPipe { } } -pub struct AsyncWriteStream { - buffer: Vec, - writer: T, -} - -impl AsyncWriteStream { - pub fn new(writer: T) -> Self { - AsyncWriteStream { - buffer: Vec::new(), - writer, - } - } -} - -#[async_trait::async_trait] -impl HostOutputStream - for AsyncWriteStream -{ - // I can get rid of the `async` here once the lock is no longer a tokio lock: - fn write(&mut self, buf: &[u8]) -> Result { - let mut bytes = core::mem::take(&mut self.buffer); - bytes.extend(buf); - - let noop_waker = noop_waker(); - let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // Make a synchronous, non-blocking call attempt to write. We are not - // going to poll this more than once, so the noop waker is appropriate. - match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { - Poll::Pending => { - // Nothing was written: buffer all of it below. - } - Poll::Ready(written) => { - // So much was written: - bytes.drain(..written?); - } - } - self.buffer = bytes; - Ok(buf.len() as u64) - } - - async fn ready(&mut self) -> Result<(), Error> { - use tokio::io::AsyncWriteExt; - let bytes = core::mem::take(&mut self.buffer); - if !bytes.is_empty() { - self.writer.write_all(bytes.as_slice()).await?; - } - Ok(()) - } -} - #[derive(Debug, Clone)] pub struct MemoryOutputPipe { buffer: std::sync::Arc>>, @@ -414,23 +203,3 @@ impl HostOutputStream for MemoryOutputPipe { Ok(()) } } - -// This implementation is basically copy-pasted out of `std` because the -// implementation there has not yet stabilized. When the `noop_waker` feature -// stabilizes, replace this with std::task::Waker::noop(). -fn noop_waker() -> Waker { - use std::task::{RawWaker, RawWakerVTable}; - const VTABLE: RawWakerVTable = RawWakerVTable::new( - // Cloning just returns a new no-op raw waker - |_| RAW, - // `wake` does nothing - |_| {}, - // `wake_by_ref` does nothing - |_| {}, - // Dropping does nothing as we don't allocate anything - |_| {}, - ); - const RAW: RawWaker = RawWaker::new(std::ptr::null(), &VTABLE); - - unsafe { Waker::from_raw(RAW) } -} diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 06239b735c59..a874fa48efc1 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -2,7 +2,7 @@ use crate::preview2::{ wasi::poll::poll::{self, Pollable}, Table, TableError, WasiView, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use std::any::Any; use std::collections::{hash_map::Entry, HashMap}; use std::future::Future; @@ -46,7 +46,7 @@ impl poll::Host for T { async fn poll_oneoff(&mut self, pollables: Vec) -> Result> { type ReadylistIndex = usize; - let mut table = self.table_mut(); + let table = self.table_mut(); let mut table_futures: HashMap)> = HashMap::new(); let mut closure_futures: Vec<(PollableFuture<'_>, Vec)> = Vec::new(); diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 708439a6f0ed..f4e41324bec6 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -173,9 +173,9 @@ impl streams::Host for T { async fn blocking_splice( &mut self, - src: InputStream, - dst: OutputStream, - len: u64, + _src: InputStream, + _dst: OutputStream, + _len: u64, ) -> Result<(u64, bool), streams::Error> { // TODO: once splice is implemented, figure out what the blocking semantics are for waiting // on src and dest here. probably just delete these blocking_ funcs altogether and defer diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 85719570f3ec..543593e913fc 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,14 +1,13 @@ use anyhow::Error; -use crate::preview2::pipe::{AsyncReadStream, AsyncWriteStream}; -use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; +use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; pub use self::stdin::*; // TODO: different cfg for windows here #[cfg(unix)] mod stdin { - use crate::preview2::pipe::{AsyncReadStream, AsyncWriteStream, AsyncFdStream}; + use crate::preview2::AsyncFdStream; pub type Stdin = AsyncFdStream; diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 31148ac5d4b1..70e82522353f 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,5 +1,7 @@ use crate::preview2::{Table, TableError}; use anyhow::Error; +use std::pin::Pin; +use std::task::{Context, Poll, Waker}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum StreamState { @@ -181,6 +183,237 @@ impl TableStreamExt for Table { } } +pub struct AsyncReadStream { + state: StreamState, + buffer: Vec, + reader: T, +} + +impl AsyncReadStream { + pub fn new(reader: T) -> Self { + AsyncReadStream { + state: StreamState::Open, + buffer: Vec::new(), + reader, + } + } +} + +#[async_trait::async_trait] +impl HostInputStream + for AsyncReadStream +{ + fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + use std::io::Write; + let l = dest.write(&self.buffer)?; + + self.buffer.drain(..l); + if !self.buffer.is_empty() { + return Ok((l as u64, StreamState::Open)); + } + + if self.state.is_closed() { + return Ok((l as u64, StreamState::Closed)); + } + + let dest = &mut dest[l..]; + let rest = if !dest.is_empty() { + let mut readbuf = tokio::io::ReadBuf::new(dest); + + let noop_waker = noop_waker(); + let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // Make a synchronous, non-blocking call attempt to read. We are not + // going to poll this more than once, so the noop waker is appropriate. + match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { + Poll::Pending => {} // Nothing was read + Poll::Ready(result) => result?, // Maybe an error occured + }; + let bytes_read = readbuf.filled().len(); + + if bytes_read == 0 { + self.state = StreamState::Closed; + } + bytes_read + } else { + 0 + }; + + Ok(((l + rest) as u64, self.state)) + } + + async fn ready(&mut self) -> Result<(), Error> { + if self.state.is_closed() { + return Ok(()); + } + + let mut bytes = core::mem::take(&mut self.buffer); + let start = bytes.len(); + bytes.resize(start + 1024, 0); + let l = + tokio::io::AsyncReadExt::read_buf(&mut self.reader, &mut &mut bytes[start..]).await?; + + // Reading 0 bytes means either there wasn't enough space in the buffer (which we + // know there is because we just resized) or that the stream has closed. Thus, we + // know the stream has closed here. + if l == 0 { + self.state = StreamState::Closed; + } + + bytes.drain(start + l..); + self.buffer = bytes; + + Ok(()) + } +} + +pub struct AsyncWriteStream { + buffer: Vec, + writer: T, +} + +impl AsyncWriteStream { + pub fn new(writer: T) -> Self { + AsyncWriteStream { + buffer: Vec::new(), + writer, + } + } +} + +#[async_trait::async_trait] +impl HostOutputStream + for AsyncWriteStream +{ + // I can get rid of the `async` here once the lock is no longer a tokio lock: + fn write(&mut self, buf: &[u8]) -> Result { + let mut bytes = core::mem::take(&mut self.buffer); + bytes.extend(buf); + + let noop_waker = noop_waker(); + let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // Make a synchronous, non-blocking call attempt to write. We are not + // going to poll this more than once, so the noop waker is appropriate. + match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { + Poll::Pending => { + // Nothing was written: buffer all of it below. + } + Poll::Ready(written) => { + // So much was written: + bytes.drain(..written?); + } + } + self.buffer = bytes; + Ok(buf.len() as u64) + } + + async fn ready(&mut self) -> Result<(), Error> { + use tokio::io::AsyncWriteExt; + let bytes = core::mem::take(&mut self.buffer); + if !bytes.is_empty() { + self.writer.write_all(bytes.as_slice()).await?; + } + Ok(()) + } +} + +// This implementation is basically copy-pasted out of `std` because the +// implementation there has not yet stabilized. When the `noop_waker` feature +// stabilizes, replace this with std::task::Waker::noop(). +fn noop_waker() -> Waker { + use std::task::{RawWaker, RawWakerVTable}; + const VTABLE: RawWakerVTable = RawWakerVTable::new( + // Cloning just returns a new no-op raw waker + |_| RAW, + // `wake` does nothing + |_| {}, + // `wake_by_ref` does nothing + |_| {}, + // Dropping does nothing as we don't allocate anything + |_| {}, + ); + const RAW: RawWaker = RawWaker::new(std::ptr::null(), &VTABLE); + + unsafe { Waker::from_raw(RAW) } +} + +#[cfg(unix)] +pub use async_fd_stream::*; + +#[cfg(unix)] +mod async_fd_stream { + use super::{HostInputStream, HostOutputStream, StreamState}; + use anyhow::Error; + use std::io::{Read, Write}; + use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; + use tokio::io::unix::AsyncFd; + + pub struct AsyncFdStream { + fd: AsyncFd, + } + + impl AsyncFdStream { + pub fn new(fd: T) -> Self { + Self { + fd: AsyncFd::new(fd).unwrap(), + } + } + } + + #[async_trait::async_trait] + impl HostInputStream for AsyncFdStream { + fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // we're done. + let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + + // TODO: how sure are we that this is non-blocking? + let read_res = file.read(dest); + + // Make sure that the file doesn't close the fd when it's dropped. + file.into_raw_fd(); + + let n = read_res?; + + // TODO: figure out when the stream should be considered closed + // TODO: figure out how to handle the error conditions from the read call above + + Ok((n as u64, StreamState::Open)) + } + + async fn ready(&mut self) -> Result<(), Error> { + let _ = self.fd.readable().await?; + Ok(()) + } + } + + #[async_trait::async_trait] + impl HostOutputStream for AsyncFdStream { + fn write(&mut self, buf: &[u8]) -> Result { + // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // we're done. + let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + + // TODO: how sure are we that this is non-blocking? + let write_res = file.write(buf); + + // Make sure that the file doesn't close the fd when it's dropped. + file.into_raw_fd(); + + let n = write_res?; + + // TODO: figure out when the stream should be considered closed + // TODO: figure out how to handle the error conditions from the write call above + + Ok(n as u64) + } + + async fn ready(&mut self) -> Result<(), Error> { + let _ = self.fd.writable().await?; + Ok(()) + } + } +} + #[cfg(test)] mod test { use super::*; From 06062591f3acf769b9e58b88069e771f61b84652 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Fri, 23 Jun 2023 17:12:06 -0700 Subject: [PATCH 034/118] Fix the sync tests --- .../tests/wasi-preview2-components-sync.rs | 86 ++++++++++--------- crates/wasi/src/preview2/poll.rs | 9 +- crates/wasi/src/preview2/stream.rs | 2 +- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index 3fe4ec209813..9c3b3243be15 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -108,124 +108,124 @@ fn run(name: &str, inherit_stdio: bool) -> Result<()> { // tests which fail. #[test_log::test] fn big_random_buf() { - run("big_random_buf", true).unwrap() + run("big_random_buf", false).unwrap() } #[test_log::test] fn clock_time_get() { - run("clock_time_get", true).unwrap() + run("clock_time_get", false).unwrap() } #[test_log::test] fn close_preopen() { - run("close_preopen", true).unwrap() + run("close_preopen", false).unwrap() } #[test_log::test] fn dangling_fd() { - run("dangling_fd", true).unwrap() + run("dangling_fd", false).unwrap() } #[test_log::test] fn dangling_symlink() { - run("dangling_symlink", true).unwrap() + run("dangling_symlink", false).unwrap() } #[test_log::test] fn directory_seek() { - run("directory_seek", true).unwrap() + run("directory_seek", false).unwrap() } #[test_log::test] fn dir_fd_op_failures() { - run("dir_fd_op_failures", true).unwrap() + run("dir_fd_op_failures", false).unwrap() } #[test_log::test] fn fd_advise() { - run("fd_advise", true).unwrap() + run("fd_advise", false).unwrap() } #[test_log::test] fn fd_filestat_get() { - run("fd_filestat_get", true).unwrap() + run("fd_filestat_get", false).unwrap() } #[test_log::test] fn fd_filestat_set() { - run("fd_filestat_set", true).unwrap() + run("fd_filestat_set", false).unwrap() } #[test_log::test] fn fd_flags_set() { - run("fd_flags_set", true).unwrap() + run("fd_flags_set", false).unwrap() } #[test_log::test] fn fd_readdir() { - run("fd_readdir", true).unwrap() + run("fd_readdir", false).unwrap() } #[test_log::test] fn file_allocate() { - run("file_allocate", true).unwrap() + run("file_allocate", false).unwrap() } #[test_log::test] fn file_pread_pwrite() { - run("file_pread_pwrite", true).unwrap() + run("file_pread_pwrite", false).unwrap() } #[test_log::test] fn file_seek_tell() { - run("file_seek_tell", true).unwrap() + run("file_seek_tell", false).unwrap() } #[test_log::test] fn file_truncation() { - run("file_truncation", true).unwrap() + run("file_truncation", false).unwrap() } #[test_log::test] fn file_unbuffered_write() { - run("file_unbuffered_write", true).unwrap() + run("file_unbuffered_write", false).unwrap() } #[test_log::test] #[cfg_attr(windows, should_panic)] fn interesting_paths() { - run("interesting_paths", true).unwrap() + run("interesting_paths", false).unwrap() } #[test_log::test] fn isatty() { - run("isatty", true).unwrap() + run("isatty", false).unwrap() } #[test_log::test] fn nofollow_errors() { - run("nofollow_errors", true).unwrap() + run("nofollow_errors", false).unwrap() } #[test_log::test] fn overwrite_preopen() { - run("overwrite_preopen", true).unwrap() + run("overwrite_preopen", false).unwrap() } #[test_log::test] fn path_exists() { - run("path_exists", true).unwrap() + run("path_exists", false).unwrap() } #[test_log::test] fn path_filestat() { - run("path_filestat", true).unwrap() + run("path_filestat", false).unwrap() } #[test_log::test] fn path_link() { - run("path_link", true).unwrap() + run("path_link", false).unwrap() } #[test_log::test] fn path_open_create_existing() { - run("path_open_create_existing", true).unwrap() + run("path_open_create_existing", false).unwrap() } #[test_log::test] fn path_open_read_write() { - run("path_open_read_write", true).unwrap() + run("path_open_read_write", false).unwrap() } #[test_log::test] fn path_open_dirfd_not_dir() { - run("path_open_dirfd_not_dir", true).unwrap() + run("path_open_dirfd_not_dir", false).unwrap() } #[test_log::test] fn path_open_missing() { - run("path_open_missing", true).unwrap() + run("path_open_missing", false).unwrap() } #[test_log::test] fn path_open_nonblock() { - run("path_open_nonblock", true).unwrap() + run("path_open_nonblock", false).unwrap() } #[test_log::test] fn path_rename_dir_trailing_slashes() { - run("path_rename_dir_trailing_slashes", true).unwrap() + run("path_rename_dir_trailing_slashes", false).unwrap() } #[test_log::test] #[should_panic] @@ -234,11 +234,11 @@ fn path_rename_file_trailing_slashes() { } #[test_log::test] fn path_rename() { - run("path_rename", true).unwrap() + run("path_rename", false).unwrap() } #[test_log::test] fn path_symlink_trailing_slashes() { - run("path_symlink_trailing_slashes", true).unwrap() + run("path_symlink_trailing_slashes", false).unwrap() } #[test_log::test] #[cfg_attr(windows, should_panic)] @@ -247,11 +247,13 @@ fn poll_oneoff_files() { } #[test_log::test] fn poll_oneoff_stdio() { + // This is the only test that should inherit stdio, or else the parallel test runner will die + // when multiple AsyncFd values are created from std::io::stdin. run("poll_oneoff_stdio", true).unwrap() } #[test_log::test] fn readlink() { - run("readlink", true).unwrap() + run("readlink", false).unwrap() } #[test_log::test] #[should_panic] @@ -260,37 +262,37 @@ fn remove_directory_trailing_slashes() { } #[test_log::test] fn remove_nonempty_directory() { - run("remove_nonempty_directory", true).unwrap() + run("remove_nonempty_directory", false).unwrap() } #[test_log::test] fn renumber() { - run("renumber", true).unwrap() + run("renumber", false).unwrap() } #[test_log::test] fn sched_yield() { - run("sched_yield", true).unwrap() + run("sched_yield", false).unwrap() } #[test_log::test] fn stdio() { - run("stdio", true).unwrap() + run("stdio", false).unwrap() } #[test_log::test] fn symlink_create() { - run("symlink_create", true).unwrap() + run("symlink_create", false).unwrap() } #[test_log::test] fn symlink_filestat() { - run("symlink_filestat", true).unwrap() + run("symlink_filestat", false).unwrap() } #[test_log::test] fn symlink_loop() { - run("symlink_loop", true).unwrap() + run("symlink_loop", false).unwrap() } #[test_log::test] fn unlink_file_trailing_slashes() { - run("unlink_file_trailing_slashes", true).unwrap() + run("unlink_file_trailing_slashes", false).unwrap() } #[test_log::test] fn path_open_preopen() { - run("path_open_preopen", true).unwrap() + run("path_open_preopen", false).unwrap() } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index a874fa48efc1..9a3396830cfe 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -128,8 +128,13 @@ pub mod sync { Ok(h) => h.block_on(f), Err(_) => { use once_cell::sync::Lazy; - static RUNTIME: Lazy = - Lazy::new(|| Builder::new_current_thread().enable_time().build().unwrap()); + static RUNTIME: Lazy = Lazy::new(|| { + Builder::new_current_thread() + .enable_time() + .enable_io() + .build() + .unwrap() + }); let _enter = RUNTIME.enter(); RUNTIME.block_on(f) } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 70e82522353f..4258aaade3f7 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -354,7 +354,7 @@ mod async_fd_stream { impl AsyncFdStream { pub fn new(fd: T) -> Self { Self { - fd: AsyncFd::new(fd).unwrap(), + fd: crate::preview2::poll::sync::block_on(async { AsyncFd::new(fd).unwrap() }), } } } From d6389e6ac5d832d7b72db5b2b08520630a86097d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 17:12:43 -0700 Subject: [PATCH 035/118] fix test uses of pipes, juggle tokio context for stdin construction --- crates/test-programs/tests/command.rs | 15 ++---- crates/test-programs/tests/reactor.rs | 12 ++--- .../tests/wasi-preview1-host-in-preview2.rs | 16 ++----- .../tests/wasi-preview2-components-sync.rs | 4 +- .../tests/wasi-preview2-components.rs | 4 +- crates/wasi/src/preview2/mod.rs | 3 +- crates/wasi/src/preview2/pipe.rs | 48 ++++++++++++++++--- crates/wasi/src/preview2/poll.rs | 5 +- crates/wasi/src/preview2/stdio.rs | 13 ++++- crates/wasi/src/preview2/stream.rs | 8 ++-- 10 files changed, 82 insertions(+), 46 deletions(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index 3f03dc3cd8d6..3f113ccfaeeb 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -1,16 +1,13 @@ use anyhow::Result; use cap_std::{ambient_authority, fs::Dir, time::Duration}; -use std::{ - io::{Cursor, Write}, - sync::Mutex, -}; +use std::{io::Write, sync::Mutex}; use wasmtime::{ component::{Component, Linker}, Config, Engine, Store, }; use wasmtime_wasi::preview2::{ clocks::{WasiMonotonicClock, WasiWallClock}, - pipe::ReadPipe, + pipe::MemoryInputPipe, wasi::command::add_to_linker, wasi::command::Command, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, @@ -176,9 +173,7 @@ async fn time() -> Result<()> { async fn stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() - .set_stdin(ReadPipe::new(Cursor::new( - "So rested he by the Tumtum tree", - ))) + .set_stdin(MemoryInputPipe::new("So rested he by the Tumtum tree")) .build(&mut table)?; let (mut store, command) = @@ -194,9 +189,7 @@ async fn stdin() -> Result<()> { async fn poll_stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() - .set_stdin(ReadPipe::new(Cursor::new( - "So rested he by the Tumtum tree", - ))) + .set_stdin(MemoryInputPipe::new("So rested he by the Tumtum tree")) .build(&mut table)?; let (mut store, command) = diff --git a/crates/test-programs/tests/reactor.rs b/crates/test-programs/tests/reactor.rs index 9cd6ececb55e..2a5347c62b67 100644 --- a/crates/test-programs/tests/reactor.rs +++ b/crates/test-programs/tests/reactor.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use std::sync::{Arc, RwLock}; use wasmtime::{ component::{Component, Linker}, Config, Engine, Store, @@ -122,14 +121,15 @@ async fn reactor_tests() -> Result<()> { // `host` and `wasi-common` crate. // Note, this works because of the add_to_linker invocations using the // `host` crate for `streams`, not because of `with` in the bindgen macro. - let write_dest: Arc>> = Arc::new(RwLock::new(Vec::new())); - let writepipe = wasmtime_wasi::preview2::pipe::WritePipe::from_shared(write_dest.clone()); - let outputstream: Box = Box::new(writepipe); - let table_ix = store.data_mut().table_mut().push(Box::new(outputstream))?; + let writepipe = preview2::pipe::MemoryOutputPipe::new(); + let table_ix = preview2::TableStreamExt::push_output_stream( + store.data_mut().table_mut(), + Box::new(writepipe.clone()), + )?; let r = reactor.call_write_strings_to(&mut store, table_ix).await?; assert_eq!(r, Ok(())); - assert_eq!(*write_dest.read().unwrap(), b"hellogussiehello againgussie"); + assert_eq!(writepipe.contents(), b"hellogussiehello againgussie"); // Show that the `with` invocation in the macro means we get to re-use the // type definitions from inside the `host` crate for these structures: diff --git a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs index 0776cf1017a5..812494c297c7 100644 --- a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs +++ b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs @@ -3,7 +3,7 @@ use anyhow::Result; use tempfile::TempDir; use wasmtime::{Config, Engine, Linker, Store}; use wasmtime_wasi::preview2::{ - pipe::WritePipe, + pipe::MemoryOutputPipe, preview1::{add_to_linker, WasiPreview1Adapter, WasiPreview1View}, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, }; @@ -30,8 +30,8 @@ pub fn prepare_workspace(exe_name: &str) -> Result { async fn run(name: &str, inherit_stdio: bool) -> Result<()> { let workspace = prepare_workspace(name)?; - let stdout = WritePipe::new_in_memory(); - let stderr = WritePipe::new_in_memory(); + let stdout = MemoryOutputPipe::new(); + let stderr = MemoryOutputPipe::new(); let r = { let mut linker = Linker::new(&ENGINE); add_to_linker(&mut linker)?; @@ -102,17 +102,11 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { }; r.map_err(move |trap: anyhow::Error| { - let stdout = stdout - .try_into_inner() - .expect("sole ref to stdout") - .into_inner(); + let stdout = stdout.try_into_inner().expect("sole ref to stdout"); if !stdout.is_empty() { println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); } - let stderr = stderr - .try_into_inner() - .expect("sole ref to stderr") - .into_inner(); + let stderr = stderr.try_into_inner().expect("sole ref to stderr"); if !stderr.is_empty() { println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); } diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index 9c3b3243be15..5d5eb56d9f35 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -87,11 +87,11 @@ fn run(name: &str, inherit_stdio: bool) -> Result<()> { }; r.map_err(move |trap: anyhow::Error| { - let stdout = stdout.finalize(); + let stdout = stdout.try_into_inner().expect("single ref to stdout"); if !stdout.is_empty() { println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); } - let stderr = stderr.finalize(); + let stderr = stderr.try_into_inner().expect("single ref to stderr"); if !stderr.is_empty() { println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); } diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index 8ae25025d257..39348b876471 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -89,11 +89,11 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { }; r.map_err(move |trap: anyhow::Error| { - let stdout = stdout.finalize(); + let stdout = stdout.try_into_inner().expect("single ref to stdout"); if !stdout.is_empty() { println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); } - let stderr = stderr.finalize(); + let stderr = stderr.try_into_inner().expect("single ref to stderr"); if !stderr.is_empty() { println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); } diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 706907e3ce9c..ce0628e2543d 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -19,11 +19,12 @@ //! `pub mod legacy` with an off-by-default feature flag, and after 2 //! releases, retire and remove that code from our tree. +// TODO: make all of these mods private pub mod clocks; mod ctx; mod error; pub(crate) mod filesystem; -pub mod pipe; +pub mod pipe; // pipe can remain a module mod poll; #[cfg(feature = "preview1-on-preview2")] pub mod preview1; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 35b3e4480586..7062715a9ebc 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -170,6 +170,39 @@ impl HostOutputStream for OutputPipe { } } +#[derive(Debug)] +pub struct MemoryInputPipe { + buffer: std::io::Cursor>, +} + +impl MemoryInputPipe { + pub fn new(bytes: impl AsRef<[u8]>) -> Self { + Self { + buffer: std::io::Cursor::new(Vec::from(bytes.as_ref())), + } + } +} + +#[async_trait::async_trait] +impl HostInputStream for MemoryInputPipe { + fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { + let nbytes = std::io::Read::read(&mut self.buffer, dest)?; + let state = if self.buffer.get_ref().len() as u64 == self.buffer.position() { + StreamState::Closed + } else { + StreamState::Open + }; + Ok((nbytes as u64, state)) + } + async fn ready(&mut self) -> Result<(), Error> { + if self.buffer.get_ref().len() as u64 > self.buffer.position() { + Ok(()) + } else { + loop {} + } + } +} + #[derive(Debug, Clone)] pub struct MemoryOutputPipe { buffer: std::sync::Arc>>, @@ -181,13 +214,14 @@ impl MemoryOutputPipe { buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), } } - pub fn finalize(self) -> Vec { - std::sync::Arc::try_unwrap(self.buffer) - .map_err(|_| ()) - .expect("more than one outstanding reference") - .into_inner() - .map_err(|_| ()) - .expect("mutex poisioned") + pub fn contents(&self) -> Vec { + self.buffer.lock().unwrap().clone() + } + pub fn try_into_inner(self) -> Result, Self> { + match std::sync::Arc::try_unwrap(self.buffer) { + Ok(m) => Ok(m.into_inner().map_err(|_| ()).expect("mutex poisioned")), + Err(buffer) => Err(Self { buffer }), + } } } diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 9a3396830cfe..b2ef924b32bf 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -125,7 +125,10 @@ pub mod sync { pub fn block_on(f: F) -> F::Output { match Handle::try_current() { - Ok(h) => h.block_on(f), + Ok(h) => { + let _enter = h.enter(); + h.block_on(f) + } Err(_) => { use once_cell::sync::Lazy; static RUNTIME: Lazy = Lazy::new(|| { diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 543593e913fc..5266a815e476 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -11,8 +11,19 @@ mod stdin { pub type Stdin = AsyncFdStream; + // FIXME this will still die if more than one is alive per process pub fn stdin() -> Stdin { - AsyncFdStream::new(std::io::stdin()) + // Must be running in a tokio context to succeed. + fn create() -> anyhow::Result { + AsyncFdStream::new(std::io::stdin()) + } + + match tokio::runtime::Handle::try_current() { + Ok(_) => create().expect("already running in a tokio context"), + Err(_) => crate::preview2::poll::sync::block_on(async { + create().expect("created a tokio context to run in") + }), + } } } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 4258aaade3f7..3a6d3e925828 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -352,10 +352,10 @@ mod async_fd_stream { } impl AsyncFdStream { - pub fn new(fd: T) -> Self { - Self { - fd: crate::preview2::poll::sync::block_on(async { AsyncFd::new(fd).unwrap() }), - } + pub fn new(fd: T) -> anyhow::Result { + Ok(Self { + fd: AsyncFd::new(fd)?, + }) } } From 60790012c16a18029491b625c44404c7be3304f5 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 17:36:58 -0700 Subject: [PATCH 036/118] add some fixmes --- crates/wasi/src/preview2/filesystem.rs | 6 ++++++ crates/wasi/src/preview2/stream.rs | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index ac2e0054eb93..497c6168f221 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -98,6 +98,7 @@ impl FileInputStream { impl HostInputStream for FileInputStream { fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread let (n, end) = read_result(self.file.read_at(buf, self.position))?; self.position = self.position.wrapping_add(n); Ok((n, end)) @@ -107,6 +108,7 @@ impl HostInputStream for FileInputStream { bufs: &mut [std::io::IoSliceMut<'a>], ) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread let (n, end) = read_result(self.file.read_vectored_at(bufs, self.position))?; self.position = self.position.wrapping_add(n); Ok((n, end)) @@ -146,6 +148,7 @@ impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread let n = self.file.write_at(buf, self.position)? as i64 as u64; self.position = self.position.wrapping_add(n); Ok(n) @@ -154,6 +157,7 @@ impl HostOutputStream for FileOutputStream { /// Vectored-I/O form of `write`. fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread let n = self.file.write_vectored_at(bufs, self.position)? as i64 as u64; self.position = self.position.wrapping_add(n); Ok(n) @@ -185,12 +189,14 @@ impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread Ok(self.file.append(buf)? as i64 as u64) } /// Vectored-I/O form of `write`. fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; + // FIXME spawn_blocking to put this on a tokio worker thread let n = self.file.append_vectored(bufs)? as i64 as u64; Ok(n) } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 3a6d3e925828..2837757b175f 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -353,6 +353,8 @@ mod async_fd_stream { impl AsyncFdStream { pub fn new(fd: T) -> anyhow::Result { + // FIXME: Use fcntl to make sure O_NONBLOCKING is set for this fd, + // otherwise the implementations below will not be correct. Ok(Self { fd: AsyncFd::new(fd)?, }) @@ -366,7 +368,7 @@ mod async_fd_stream { // we're done. let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - // TODO: how sure are we that this is non-blocking? + // Ensured this is nonblocking at construction of AsyncFdStream. let read_res = file.read(dest); // Make sure that the file doesn't close the fd when it's dropped. @@ -393,7 +395,7 @@ mod async_fd_stream { // we're done. let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - // TODO: how sure are we that this is non-blocking? + // Ensured this is nonblocking at construction of AsyncFdStream. let write_res = file.write(buf); // Make sure that the file doesn't close the fd when it's dropped. @@ -401,7 +403,6 @@ mod async_fd_stream { let n = write_res?; - // TODO: figure out when the stream should be considered closed // TODO: figure out how to handle the error conditions from the write call above Ok(n as u64) From 2924bba5c7b90fe85923849b4ab844f2eb36c424 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 18:09:58 -0700 Subject: [PATCH 037/118] the future i named Never is defined in futures-util as pending which is a better name --- Cargo.lock | 1 + Cargo.toml | 1 + crates/wasi/Cargo.toml | 2 ++ crates/wasi/src/preview2/pipe.rs | 2 +- crates/wasi/src/preview2/stdio.rs | 17 ++--------------- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0fe7e97f40d..67eb572f4ae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4235,6 +4235,7 @@ dependencies = [ "cap-std", "cap-time-ext", "fs-set-times", + "futures-util", "io-extras", "libc", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 82cc5235afe9..4a8af6227172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,6 +240,7 @@ filecheck = "0.5.0" libc = "0.2.60" file-per-thread-logger = "0.2.0" tokio = { version = "1.26.0" } +futures-util = { version = "0.3.27", default-features = false } [features] default = [ diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index f0b63267fe1f..f8ce51c1a682 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -34,6 +34,7 @@ bitflags = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } system-interface = { workspace = true, optional = true} rustix = { workspace = true, features = ["net"], optional = true} +futures-util = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["fs"] } @@ -62,6 +63,7 @@ preview2 = [ 'dep:system-interface', 'dep:rustix', 'dep:tokio', + 'dep:futures-util', ] preview1-on-preview2 = [ "preview2", diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 7062715a9ebc..6c4b00af4640 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -198,7 +198,7 @@ impl HostInputStream for MemoryInputPipe { if self.buffer.get_ref().len() as u64 > self.buffer.position() { Ok(()) } else { - loop {} + futures_util::future::pending().await } } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 5266a815e476..5dece29b677d 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -14,7 +14,7 @@ mod stdin { // FIXME this will still die if more than one is alive per process pub fn stdin() -> Stdin { // Must be running in a tokio context to succeed. - fn create() -> anyhow::Result { + fn create() -> anyhow::Result> { AsyncFdStream::new(std::io::stdin()) } @@ -47,20 +47,7 @@ impl HostInputStream for EmptyStream { } async fn ready(&mut self) -> Result<(), Error> { - struct Never; - - impl std::future::Future for Never { - type Output = anyhow::Result<()>; - fn poll( - self: std::pin::Pin<&mut Self>, - _ctx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - std::task::Poll::Pending - } - } - - // This stream is never ready for reading. - Never.await + futures_util::future::pending().await } } From 7dd2ef99d78773f635fd89aa46ad488e80b07791 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 19:31:23 -0700 Subject: [PATCH 038/118] i believe this is a correct implementation of one global stdin resource --- crates/wasi/Cargo.toml | 2 +- crates/wasi/src/preview2/stdio.rs | 55 ++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index f8ce51c1a682..f7eb80253db9 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -22,7 +22,7 @@ wiggle = { workspace = true, optional = true } libc = { workspace = true } once_cell = { workspace = true } -tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "net"] } +tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 5dece29b677d..5d6b4ab7b3db 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,27 +2,58 @@ use anyhow::Error; use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; -pub use self::stdin::*; +pub use self::stdin::{stdin, Stdin}; // TODO: different cfg for windows here #[cfg(unix)] mod stdin { - use crate::preview2::AsyncFdStream; + use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; + use anyhow::Error; + use std::sync::OnceLock; + use tokio::sync::Mutex; - pub type Stdin = AsyncFdStream; + type GlobalStdin = Mutex>; + static STDIN: OnceLock = OnceLock::new(); + + // Must be running in a tokio context to succeed. + fn create() -> anyhow::Result { + Ok(Mutex::new(AsyncFdStream::new(std::io::stdin())?)) + } + + pub struct Stdin; + impl Stdin { + fn get_global() -> &'static GlobalStdin { + match tokio::runtime::Handle::try_current() { + Ok(_) => STDIN.get_or_init(|| { + create().expect("creating AsyncFd for stdin in existing tokio context") + }), + Err(_) => STDIN.get_or_init(|| { + crate::preview2::poll::sync::block_on(async { + create().expect("creating AsyncFd for stdin in internal tokio context") + }) + }), + } + } + } - // FIXME this will still die if more than one is alive per process pub fn stdin() -> Stdin { - // Must be running in a tokio context to succeed. - fn create() -> anyhow::Result> { - AsyncFdStream::new(std::io::stdin()) + Stdin + } + + #[async_trait::async_trait] + impl HostInputStream for Stdin { + fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { + let mut r = move || Self::get_global().blocking_lock().read(buf); + // If we are currently in a tokio context, blocking_lock will panic unless inside a + // block_in_place: + match tokio::runtime::Handle::try_current() { + Ok(_) => tokio::task::block_in_place(r), + Err(_) => r(), + } } - match tokio::runtime::Handle::try_current() { - Ok(_) => create().expect("already running in a tokio context"), - Err(_) => crate::preview2::poll::sync::block_on(async { - create().expect("created a tokio context to run in") - }), + async fn ready(&mut self) -> Result<(), Error> { + Self::get_global().lock().await.ready().await } } } From 983dfdf0230adf7370857a6a5504da247eab1709 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 19:39:41 -0700 Subject: [PATCH 039/118] move unix stdin to its own file --- crates/wasi/src/preview2/stdio.rs | 58 ++------------------------ crates/wasi/src/preview2/stdio/unix.rs | 54 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 crates/wasi/src/preview2/stdio/unix.rs diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 5d6b4ab7b3db..b531a5342be1 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,61 +2,11 @@ use anyhow::Error; use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; -pub use self::stdin::{stdin, Stdin}; - -// TODO: different cfg for windows here +// TODO: different cfg for windows support #[cfg(unix)] -mod stdin { - use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; - use anyhow::Error; - use std::sync::OnceLock; - use tokio::sync::Mutex; - - type GlobalStdin = Mutex>; - static STDIN: OnceLock = OnceLock::new(); - - // Must be running in a tokio context to succeed. - fn create() -> anyhow::Result { - Ok(Mutex::new(AsyncFdStream::new(std::io::stdin())?)) - } - - pub struct Stdin; - impl Stdin { - fn get_global() -> &'static GlobalStdin { - match tokio::runtime::Handle::try_current() { - Ok(_) => STDIN.get_or_init(|| { - create().expect("creating AsyncFd for stdin in existing tokio context") - }), - Err(_) => STDIN.get_or_init(|| { - crate::preview2::poll::sync::block_on(async { - create().expect("creating AsyncFd for stdin in internal tokio context") - }) - }), - } - } - } - - pub fn stdin() -> Stdin { - Stdin - } - - #[async_trait::async_trait] - impl HostInputStream for Stdin { - fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { - let mut r = move || Self::get_global().blocking_lock().read(buf); - // If we are currently in a tokio context, blocking_lock will panic unless inside a - // block_in_place: - match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::block_in_place(r), - Err(_) => r(), - } - } - - async fn ready(&mut self) -> Result<(), Error> { - Self::get_global().lock().await.ready().await - } - } -} +mod unix; +#[cfg(unix)] +pub use self::unix::{stdin, Stdin}; pub type Stdout = AsyncWriteStream; diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs new file mode 100644 index 000000000000..92af30013638 --- /dev/null +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -0,0 +1,54 @@ +use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; +use anyhow::Error; +use std::sync::OnceLock; +// We use a tokio Mutex because, in ready(), the mutex needs to be held +// across an await. +use tokio::sync::Mutex; + +// We need a single global instance of the AsyncFd because creating +// this instance registers the process's stdin fd with epoll, which will +// return an error if an fd is registered more than once. +type GlobalStdin = Mutex>; +static STDIN: OnceLock = OnceLock::new(); + +fn create() -> anyhow::Result { + Ok(Mutex::new(AsyncFdStream::new(std::io::stdin())?)) +} + +pub struct Stdin; +impl Stdin { + fn get_global() -> &'static GlobalStdin { + // Creation must be running in a tokio context to succeed. + match tokio::runtime::Handle::try_current() { + Ok(_) => STDIN.get_or_init(|| { + create().expect("creating AsyncFd for stdin in existing tokio context") + }), + Err(_) => STDIN.get_or_init(|| { + crate::preview2::poll::sync::block_on(async { + create().expect("creating AsyncFd for stdin in internal tokio context") + }) + }), + } + } +} + +pub fn stdin() -> Stdin { + Stdin +} + +#[async_trait::async_trait] +impl HostInputStream for Stdin { + fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { + let mut r = move || Self::get_global().blocking_lock().read(buf); + // If we are currently in a tokio context, blocking_lock will panic unless inside a + // block_in_place: + match tokio::runtime::Handle::try_current() { + Ok(_) => tokio::task::block_in_place(r), + Err(_) => r(), + } + } + + async fn ready(&mut self) -> Result<(), Error> { + Self::get_global().lock().await.ready().await + } +} From 25d560b8069400c08a9925fc76da02923669816e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 26 Jun 2023 19:49:55 -0700 Subject: [PATCH 040/118] make most of the mods private --- crates/test-programs/tests/command.rs | 7 ++----- crates/wasi/src/preview2/mod.rs | 18 +++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index 3f113ccfaeeb..ec9fe092b3ce 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -6,11 +6,8 @@ use wasmtime::{ Config, Engine, Store, }; use wasmtime_wasi::preview2::{ - clocks::{WasiMonotonicClock, WasiWallClock}, - pipe::MemoryInputPipe, - wasi::command::add_to_linker, - wasi::command::Command, - DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, + pipe::MemoryInputPipe, wasi::command::add_to_linker, wasi::command::Command, DirPerms, + FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiMonotonicClock, WasiView, WasiWallClock, }; lazy_static::lazy_static! { diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index ce0628e2543d..306655909681 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -19,20 +19,19 @@ //! `pub mod legacy` with an off-by-default feature flag, and after 2 //! releases, retire and remove that code from our tree. -// TODO: make all of these mods private -pub mod clocks; +mod clocks; mod ctx; mod error; -pub(crate) mod filesystem; -pub mod pipe; // pipe can remain a module +mod filesystem; +pub mod pipe; mod poll; #[cfg(feature = "preview1-on-preview2")] pub mod preview1; -pub mod preview2; -pub mod random; -pub mod stdio; -pub mod stream; -pub mod table; +mod preview2; +mod random; +mod stdio; +mod stream; +mod table; pub mod wasi; pub use self::clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; @@ -40,6 +39,7 @@ pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; +pub use self::random::{thread_rng, Deterministic}; #[cfg(unix)] pub use self::stream::AsyncFdStream; pub use self::stream::{ From 7e934327bb60dd880eb3eae4b7256deb659678b1 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 27 Jun 2023 08:53:08 -0700 Subject: [PATCH 041/118] fix build - we are skipping rust 1.70 due to llvm regressions in s390x and riscv64 which are fixed in 1.71 and will not be backported --- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/stdio/unix.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 306655909681..0c5bb9fe1302 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -32,7 +32,7 @@ mod random; mod stdio; mod stream; mod table; -pub mod wasi; +pub mod wasi; // TODO: i can move the wasi mod out to this level. pub use self::clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index 92af30013638..dc9c4e7c845d 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -1,6 +1,10 @@ use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; use anyhow::Error; -use std::sync::OnceLock; + +// wasmtime cant use std::sync::OnceLock yet because of a llvm regression in +// 1.70. when 1.71 is released, we can switch to using std here. +use once_cell::sync::OnceCell as OnceLock; + // We use a tokio Mutex because, in ready(), the mutex needs to be held // across an await. use tokio::sync::Mutex; From 66d58902cc2ce788d024c65f8ed9cca86dda6c20 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 27 Jun 2023 14:38:45 -0700 Subject: [PATCH 042/118] preview1-in-preview2: use async funcs for io, and the async io interface prtest:full --- crates/wasi/src/preview2/preview1/mod.rs | 46 +++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index 7a9af6164af8..ec1dfdd79d64 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1,10 +1,9 @@ use crate::preview2::filesystem::TableFsExt; use crate::preview2::preview2::filesystem::TableReaddirExt; use crate::preview2::wasi::cli_base::{preopens, stderr, stdin, stdout}; -use crate::preview2::wasi::clocks::monotonic_clock; -use crate::preview2::wasi::clocks::wall_clock; +use crate::preview2::wasi::clocks::{monotonic_clock, wall_clock}; use crate::preview2::wasi::filesystem::filesystem; -use crate::preview2::wasi::sync_io::io::streams; +use crate::preview2::wasi::io::streams; use crate::preview2::{wasi, TableError, WasiView}; use anyhow::{anyhow, bail, Context}; use std::borrow::Borrow; @@ -324,7 +323,7 @@ pub fn add_to_linker< + wasi::filesystem::filesystem::Host + wasi::sync_io::poll::poll::Host + wasi::random::random::Host - + wasi::sync_io::io::streams::Host + + wasi::io::streams::Host + wasi::clocks::monotonic_clock::Host + wasi::clocks::wall_clock::Host, >( @@ -339,6 +338,7 @@ pub fn add_to_linker< // to this module. wiggle::from_witx!({ witx: ["$CARGO_MANIFEST_DIR/witx/wasi_snapshot_preview1.witx"], + async: { wasi_snapshot_preview1::{fd_close, fd_read, fd_pread, fd_write, fd_pwrite, poll_oneoff} }, errors: { errno => trappable Error }, }); @@ -611,6 +611,7 @@ fn first_non_empty_iovec<'a>( .transpose() } +#[async_trait::async_trait] // Implement the WasiSnapshotPreview1 trait using only the traits that are // required for T, i.e., in terms of the preview 2 wit interface, and state // stored in the WasiPreview1Adapter struct. @@ -622,7 +623,7 @@ impl< + wasi::filesystem::filesystem::Host + wasi::sync_io::poll::poll::Host + wasi::random::random::Host - + wasi::sync_io::io::streams::Host + + wasi::io::streams::Host + wasi::clocks::monotonic_clock::Host + wasi::clocks::wall_clock::Host, > wasi_snapshot_preview1::WasiSnapshotPreview1 for T @@ -790,7 +791,7 @@ impl< /// Close a file descriptor. /// NOTE: This is similar to `close` in POSIX. #[instrument(skip(self))] - fn fd_close(&mut self, fd: types::Fd) -> Result<(), types::Error> { + async fn fd_close(&mut self, fd: types::Fd) -> Result<(), types::Error> { let desc = self .transact()? .descriptors @@ -800,9 +801,11 @@ impl< .clone(); match desc { Descriptor::Stdin(stream) => streams::Host::drop_input_stream(self, stream) + .await .context("failed to call `drop-input-stream`"), Descriptor::Stdout(stream) | Descriptor::Stderr(stream) => { streams::Host::drop_output_stream(self, stream) + .await .context("failed to call `drop-output-stream`") } Descriptor::File(File { fd, .. }) | Descriptor::PreopenDirectory((fd, _)) => self @@ -1038,7 +1041,7 @@ impl< /// Read from a file descriptor. /// NOTE: This is similar to `readv` in POSIX. #[instrument(skip(self))] - fn fd_read<'a>( + async fn fd_read<'a>( &mut self, fd: types::Fd, iovs: &types::IovecArray<'a>, @@ -1063,9 +1066,9 @@ impl< })?; let max = buf.len().try_into().unwrap_or(u64::MAX); let (read, end) = if blocking { - streams::Host::blocking_read(self, stream, max) + streams::Host::blocking_read(self, stream, max).await } else { - streams::Host::read(self, stream, max) + streams::Host::read(self, stream, max).await } .map_err(|_| types::Errno::Io)?; @@ -1081,6 +1084,7 @@ impl< }; let (read, end) = streams::Host::read(self, stream, buf.len().try_into().unwrap_or(u64::MAX)) + .await .map_err(|_| types::Errno::Io)?; (buf, read, end) } @@ -1101,7 +1105,7 @@ impl< /// Read from a file descriptor, without using and updating the file descriptor's offset. /// NOTE: This is similar to `preadv` in POSIX. #[instrument(skip(self))] - fn fd_pread<'a>( + async fn fd_pread<'a>( &mut self, fd: types::Fd, iovs: &types::IovecArray<'a>, @@ -1121,9 +1125,9 @@ impl< })?; let max = buf.len().try_into().unwrap_or(u64::MAX); let (read, end) = if blocking { - streams::Host::blocking_read(self, stream, max) + streams::Host::blocking_read(self, stream, max).await } else { - streams::Host::read(self, stream, max) + streams::Host::read(self, stream, max).await } .map_err(|_| types::Errno::Io)?; @@ -1150,7 +1154,7 @@ impl< /// Write to a file descriptor. /// NOTE: This is similar to `writev` in POSIX. #[instrument(skip(self))] - fn fd_write<'a>( + async fn fd_write<'a>( &mut self, fd: types::Fd, ciovs: &types::CiovecArray<'a>, @@ -1183,9 +1187,9 @@ impl< (stream, position) }; let n = if blocking { - streams::Host::blocking_write(self, stream, buf) + streams::Host::blocking_write(self, stream, buf).await } else { - streams::Host::write(self, stream, buf) + streams::Host::write(self, stream, buf).await } .map_err(|_| types::Errno::Io)?; if !append { @@ -1198,7 +1202,9 @@ impl< let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - streams::Host::write(self, stream, buf).map_err(|_| types::Errno::Io)? + streams::Host::write(self, stream, buf) + .await + .map_err(|_| types::Errno::Io)? } _ => return Err(types::Errno::Badf.into()), } @@ -1210,7 +1216,7 @@ impl< /// Write to a file descriptor, without using and updating the file descriptor's offset. /// NOTE: This is similar to `pwritev` in POSIX. #[instrument(skip(self))] - fn fd_pwrite<'a>( + async fn fd_pwrite<'a>( &mut self, fd: types::Fd, ciovs: &types::CiovecArray<'a>, @@ -1228,9 +1234,9 @@ impl< .unwrap_or_else(types::Error::trap) })?; if blocking { - streams::Host::blocking_write(self, stream, buf) + streams::Host::blocking_write(self, stream, buf).await } else { - streams::Host::write(self, stream, buf) + streams::Host::write(self, stream, buf).await } .map_err(|_| types::Errno::Io)? } @@ -1725,7 +1731,7 @@ impl< #[allow(unused_variables)] #[instrument(skip(self))] - fn poll_oneoff<'a>( + async fn poll_oneoff<'a>( &mut self, subs: &GuestPtr<'a, types::Subscription>, events: &GuestPtr<'a, types::Event>, From 25a1523aaf07a51c31600c89e7892e381d202332 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 27 Jun 2023 14:38:45 -0700 Subject: [PATCH 043/118] windows stdin support --- crates/wasi/src/preview2/stdio.rs | 5 ++ crates/wasi/src/preview2/stdio/windows.rs | 90 +++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 crates/wasi/src/preview2/stdio/windows.rs diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index b531a5342be1..12bf5b1f27f2 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -8,6 +8,11 @@ mod unix; #[cfg(unix)] pub use self::unix::{stdin, Stdin}; +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use self::windows::{stdin, Stdin}; + pub type Stdout = AsyncWriteStream; pub fn stdout() -> Stdout { diff --git a/crates/wasi/src/preview2/stdio/windows.rs b/crates/wasi/src/preview2/stdio/windows.rs new file mode 100644 index 000000000000..b57e7c7b4634 --- /dev/null +++ b/crates/wasi/src/preview2/stdio/windows.rs @@ -0,0 +1,90 @@ +use crate::preview2::{HostInputStream, StreamState}; +use anyhow::{Context, Error}; + +// wasmtime cant use std::sync::OnceLock yet because of a llvm regression in +// 1.70. when 1.71 is released, we can switch to using std here. +use once_cell::sync::OnceCell as OnceLock; + +// We use a tokio Mutex because, in ready(), the mutex needs to be held +// across an await. +use tokio::sync::Mutex; + +// We need a single global instance of the AsyncFd because creating +// this instance registers the process's stdin fd with epoll, which will +// return an error if an fd is registered more than once. +struct GlobalStdin { + tx: tokio::sync::mpsc::Sender>>, + // FIXME use a Watch to check for readiness instead of sending a oneshot sender +} +static STDIN: OnceLock> = OnceLock::new(); + +fn create() -> Mutex { + let (tx, mut rx) = + tokio::sync::mpsc::channel::>>(1); + std::thread::spawn(move || { + use std::io::BufRead; + // A client is interested in stdin's readiness + while let Some(msg) = rx.blocking_recv() { + // Fill buf - can we skip this if its + // already filled? + // also, this could block forever and the + // client could give up. in that case, + // another client may want to start waiting + let r = std::io::stdin() + .lock() + .fill_buf() + .map(|_| ()) + .map_err(anyhow::Error::from); + // tell the client stdin is ready for reading + let _ = msg.send(r); + } + }); + + Mutex::new(GlobalStdin { tx }) +} + +pub struct Stdin; +impl Stdin { + fn get_global() -> &'static Mutex { + // Creation must be running in a tokio context to succeed. + match tokio::runtime::Handle::try_current() { + Ok(_) => STDIN.get_or_init(|| create()), + Err(_) => { + STDIN.get_or_init(|| crate::preview2::poll::sync::block_on(async { create() })) + } + } + } +} + +pub fn stdin() -> Stdin { + Stdin +} + +#[async_trait::async_trait] +impl HostInputStream for Stdin { + fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { + use std::io::Read; + let mut r = move || { + let nbytes = std::io::stdin().read(buf)?; + // FIXME handle eof + Ok((nbytes as u64, StreamState::Open)) + }; + // If we are currently in a tokio context, block: + match tokio::runtime::Handle::try_current() { + Ok(_) => tokio::task::block_in_place(r), + Err(_) => r(), + } + } + + async fn ready(&mut self) -> Result<(), Error> { + let (result_tx, rx) = tokio::sync::oneshot::channel::>(); + Self::get_global() + .lock() + .await + .tx + .send(result_tx) + .await // Could hang here if we another wait on this was canceled?? + .context("sending message to worker thread")?; + rx.await.expect("channel is always alive") + } +} From 75d24f7f99e78ea9d0805271b526e4b44516d0fd Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 27 Jun 2023 16:35:22 -0700 Subject: [PATCH 044/118] done! --- crates/wasi/src/preview2/stdio.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 12bf5b1f27f2..59de42108026 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -2,7 +2,6 @@ use anyhow::Error; use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; -// TODO: different cfg for windows support #[cfg(unix)] mod unix; #[cfg(unix)] From d0d67bc2a9654bb28c5542bdf76f86f14db6a352 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 28 Jun 2023 11:40:39 -0700 Subject: [PATCH 045/118] table ext functions: fix tests --- crates/wasi/src/preview2/stream.rs | 33 +++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 2837757b175f..ccd43ab50abd 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -418,21 +418,44 @@ mod async_fd_stream { #[cfg(test)] mod test { use super::*; - use crate::preview2::pipe::{ReadPipe, WritePipe}; #[test] fn input_stream_in_table() { - let empty_pipe = ReadPipe::new(std::io::empty()); + struct DummyInputStream; + #[async_trait::async_trait] + impl HostInputStream for DummyInputStream { + fn read(&mut self, _: &mut [u8]) -> Result<(u64, StreamState), Error> { + unimplemented!(); + } + async fn ready(&mut self) -> Result<(), Error> { + unimplemented!(); + } + } + + let dummy = DummyInputStream; let mut table = Table::new(); - let ix = table.push_input_stream(Box::new(empty_pipe)).unwrap(); + // Show that we can put an input stream in the table, and both retrieval functions work: + let ix = table.push_input_stream(Box::new(dummy)).unwrap(); let _ = table.get_input_stream(ix).unwrap(); let _ = table.get_input_stream_mut(ix).unwrap(); } #[test] fn output_stream_in_table() { - let dev_null = WritePipe::new(std::io::sink()); + struct DummyOutputStream; + #[async_trait::async_trait] + impl HostOutputStream for DummyOutputStream { + fn write(&mut self, _: &[u8]) -> Result { + unimplemented!(); + } + async fn ready(&mut self) -> Result<(), Error> { + unimplemented!(); + } + } + + let dummy = DummyOutputStream; let mut table = Table::new(); - let ix = table.push_output_stream(Box::new(dev_null)).unwrap(); + // Show that we can put an output stream in the table, and both retrieval functions work: + let ix = table.push_output_stream(Box::new(dummy)).unwrap(); let _ = table.get_output_stream(ix).unwrap(); let _ = table.get_output_stream_mut(ix).unwrap(); } From 6cc97da16f85310bc0b974553e3c2071f6a301c1 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 28 Jun 2023 13:53:59 -0700 Subject: [PATCH 046/118] tests: expect poll_oneoff_{files,stdio} to pass on all platforms --- crates/test-programs/tests/wasi-preview2-components-sync.rs | 3 --- crates/test-programs/tests/wasi-preview2-components.rs | 3 --- 2 files changed, 6 deletions(-) diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index 5d5eb56d9f35..a7959153d5b1 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -241,14 +241,11 @@ fn path_symlink_trailing_slashes() { run("path_symlink_trailing_slashes", false).unwrap() } #[test_log::test] -#[cfg_attr(windows, should_panic)] fn poll_oneoff_files() { run("poll_oneoff_files", false).unwrap() } #[test_log::test] fn poll_oneoff_stdio() { - // This is the only test that should inherit stdio, or else the parallel test runner will die - // when multiple AsyncFd values are created from std::io::stdin. run("poll_oneoff_stdio", true).unwrap() } #[test_log::test] diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index 39348b876471..26e73e7ad758 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -245,13 +245,10 @@ async fn path_symlink_trailing_slashes() { run("path_symlink_trailing_slashes", true).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] -#[cfg_attr(windows, should_panic)] async fn poll_oneoff_files() { run("poll_oneoff_files", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] -// This is a known bug with the preview 2 implementation on Windows: -#[cfg_attr(windows, should_panic)] async fn poll_oneoff_stdio() { run("poll_oneoff_stdio", true).await.unwrap() } From 9d068712bab929a9257dee31df39f78abd557979 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 28 Jun 2023 14:53:12 -0700 Subject: [PATCH 047/118] export the bindings under wasmtime_wasi::preview2::bindings rather than preview2::wasi. and command moves to wasmtime_wasi::preview2::command as well. --- crates/wasi/src/preview2/command.rs | 91 +++++++++++++++++++ crates/wasi/src/preview2/mod.rs | 87 +++++++++++++++++- crates/wasi/src/preview2/poll.rs | 6 +- crates/wasi/src/preview2/preview1/mod.rs | 46 +++++----- crates/wasi/src/preview2/preview2/clocks.rs | 2 +- crates/wasi/src/preview2/preview2/env.rs | 10 +- crates/wasi/src/preview2/preview2/exit.rs | 2 +- .../wasi/src/preview2/preview2/filesystem.rs | 9 +- crates/wasi/src/preview2/preview2/io.rs | 10 +- crates/wasi/src/preview2/preview2/random.rs | 4 +- crates/wasi/src/preview2/wasi/command.rs | 91 ------------------- crates/wasi/src/preview2/wasi/mod.rs | 80 ---------------- 12 files changed, 214 insertions(+), 224 deletions(-) create mode 100644 crates/wasi/src/preview2/command.rs delete mode 100644 crates/wasi/src/preview2/wasi/command.rs delete mode 100644 crates/wasi/src/preview2/wasi/mod.rs diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs new file mode 100644 index 000000000000..155cb83afe05 --- /dev/null +++ b/crates/wasi/src/preview2/command.rs @@ -0,0 +1,91 @@ +use crate::preview2::WasiView; + +wasmtime::component::bindgen!({ + world: "wasi:preview/command", + tracing: true, + async: true, + trappable_error_type: { + "filesystem"::"error-code": Error, + "streams"::"stream-error": Error, + }, + with: { + "wasi:filesystem/filesystem": crate::preview2::bindings::filesystem::filesystem, + "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, + "wasi:poll/poll": crate::preview2::bindings::poll::poll, + "wasi:io/streams": crate::preview2::bindings::io::streams, + "wasi:clocks/timezone": crate::preview2::bindings::clocks::timezone, + "wasi:clocks/wall_clock": crate::preview2::bindings::clocks::wall_clock, + "wasi:random/random": crate::preview2::bindings::random::random, + "wasi:cli_base/environment": crate::preview2::bindings::cli_base::environment, + "wasi:cli_base/exit": crate::preview2::bindings::cli_base::exit, + "wasi:cli_base/preopens": crate::preview2::bindings::cli_base::preopens, + "wasi:cli_base/stdin": crate::preview2::bindings::cli_base::stdin, + "wasi:cli_base/stdout": crate::preview2::bindings::cli_base::stdout, + "wasi:cli_base/stderr": crate::preview2::bindings::cli_base::stderr, + }, +}); + +pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> anyhow::Result<()> { + crate::preview2::bindings::clocks::wall_clock::add_to_linker(l, |t| t)?; + crate::preview2::bindings::clocks::monotonic_clock::add_to_linker(l, |t| t)?; + crate::preview2::bindings::clocks::timezone::add_to_linker(l, |t| t)?; + crate::preview2::bindings::filesystem::filesystem::add_to_linker(l, |t| t)?; + crate::preview2::bindings::poll::poll::add_to_linker(l, |t| t)?; + crate::preview2::bindings::io::streams::add_to_linker(l, |t| t)?; + crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::exit::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::environment::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::preopens::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stdin::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stdout::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stderr::add_to_linker(l, |t| t)?; + Ok(()) +} + +pub mod sync { + use crate::preview2::WasiView; + + wasmtime::component::bindgen!({ + world: "wasi:preview/command", + tracing: true, + async: false, + trappable_error_type: { + "filesystem"::"error-code": Error, + "streams"::"stream-error": Error, + }, + with: { + "wasi:filesystem/filesystem": crate::preview2::bindings::filesystem::filesystem, + "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, + "wasi:poll/poll": crate::preview2::bindings::sync_io::poll::poll, + "wasi:io/streams": crate::preview2::bindings::sync_io::io::streams, + "wasi:clocks/timezone": crate::preview2::bindings::clocks::timezone, + "wasi:clocks/wall_clock": crate::preview2::bindings::clocks::wall_clock, + "wasi:random/random": crate::preview2::bindings::random::random, + "wasi:cli_base/environment": crate::preview2::bindings::cli_base::environment, + "wasi:cli_base/exit": crate::preview2::bindings::cli_base::exit, + "wasi:cli_base/preopens": crate::preview2::bindings::cli_base::preopens, + "wasi:cli_base/stdin": crate::preview2::bindings::cli_base::stdin, + "wasi:cli_base/stdout": crate::preview2::bindings::cli_base::stdout, + "wasi:cli_base/stderr": crate::preview2::bindings::cli_base::stderr, + }, + }); + + pub fn add_to_linker( + l: &mut wasmtime::component::Linker, + ) -> anyhow::Result<()> { + crate::preview2::bindings::clocks::wall_clock::add_to_linker(l, |t| t)?; + crate::preview2::bindings::clocks::monotonic_clock::add_to_linker(l, |t| t)?; + crate::preview2::bindings::clocks::timezone::add_to_linker(l, |t| t)?; + crate::preview2::bindings::filesystem::filesystem::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sync_io::poll::poll::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sync_io::io::streams::add_to_linker(l, |t| t)?; + crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::exit::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::environment::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::preopens::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stdin::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stdout::add_to_linker(l, |t| t)?; + crate::preview2::bindings::cli_base::stderr::add_to_linker(l, |t| t)?; + Ok(()) + } +} diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 0c5bb9fe1302..2b15c1d032e9 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -7,10 +7,6 @@ //! //! Presently, this crate is experimental. We don't yet recommend you use it //! in production. Specifically: -//! * it does not yet support a synchronous rust embedding -//! * polling and streams need a redesign. IO that currently should be -//! non-blocking may be blocking. poll probably doesn't work at all. -//! * its internal organization could use some love //! * the wit files in tree describing preview 2 are not faithful to the //! standards repos //! @@ -20,6 +16,7 @@ //! releases, retire and remove that code from our tree. mod clocks; +pub mod command; mod ctx; mod error; mod filesystem; @@ -32,7 +29,6 @@ mod random; mod stdio; mod stream; mod table; -pub mod wasi; // TODO: i can move the wasi mod out to this level. pub use self::clocks::{WasiClocks, WasiMonotonicClock, WasiWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; @@ -49,3 +45,84 @@ pub use self::stream::{ pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; + +pub mod bindings { + pub mod sync_io { + pub(crate) mod _internal { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:poll/poll + import wasi:io/streams + ", + tracing: true, + trappable_error_type: { + "streams"::"stream-error": Error, + } + }); + } + pub use self::_internal::wasi::{io, poll}; + + impl From for io::streams::StreamError { + fn from(_other: super::io::streams::StreamError) -> Self { + // There are no cases for this record. + Self {} + } + } + + impl From for io::streams::Error { + fn from(other: super::io::streams::Error) -> Self { + match other.downcast() { + Ok(se) => io::streams::Error::from(io::streams::StreamError::from(se)), + Err(e) => io::streams::Error::trap(e), + } + } + } + } + + pub(crate) mod _internal_io { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:poll/poll + import wasi:io/streams + ", + tracing: true, + async: true, + trappable_error_type: { + "streams"::"stream-error": Error, + } + }); + } + pub use self::_internal_io::wasi::{io, poll}; + pub(crate) mod _internal_rest { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/filesystem + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:cli-base/environment + import wasi:cli-base/preopens + import wasi:cli-base/exit + import wasi:cli-base/stdin + import wasi:cli-base/stdout + import wasi:cli-base/stderr + ", + tracing: true, + trappable_error_type: { + "filesystem"::"error-code": Error, + "streams"::"stream-error": Error, + }, + with: { + "wasi:poll/poll": crate::preview2::bindings::poll::poll, + "wasi:io/streams": crate::preview2::bindings::io::streams + } + }); + } + pub use self::_internal_rest::wasi::*; +} diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 5a5af35bcf09..314c133b407e 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -1,5 +1,5 @@ use crate::preview2::{ - wasi::poll::poll::{self, Pollable}, + bindings::poll::poll::{self, Pollable}, Table, TableError, WasiView, }; use anyhow::Result; @@ -115,8 +115,8 @@ impl poll::Host for T { pub mod sync { use crate::preview2::{ - wasi::poll::poll::Host as AsyncHost, - wasi::sync_io::poll::poll::{self, Pollable}, + bindings::poll::poll::Host as AsyncHost, + bindings::sync_io::poll::poll::{self, Pollable}, WasiView, }; use anyhow::Result; diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index ec1dfdd79d64..1876544bfcca 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1,10 +1,10 @@ +use crate::preview2::bindings::cli_base::{preopens, stderr, stdin, stdout}; +use crate::preview2::bindings::clocks::{monotonic_clock, wall_clock}; +use crate::preview2::bindings::filesystem::filesystem; +use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::TableFsExt; use crate::preview2::preview2::filesystem::TableReaddirExt; -use crate::preview2::wasi::cli_base::{preopens, stderr, stdin, stdout}; -use crate::preview2::wasi::clocks::{monotonic_clock, wall_clock}; -use crate::preview2::wasi::filesystem::filesystem; -use crate::preview2::wasi::io::streams; -use crate::preview2::{wasi, TableError, WasiView}; +use crate::preview2::{bindings, TableError, WasiView}; use anyhow::{anyhow, bail, Context}; use std::borrow::Borrow; use std::cell::Cell; @@ -317,15 +317,15 @@ impl WasiPreview1ViewExt for T {} pub fn add_to_linker< T: WasiPreview1View - + wasi::cli_base::environment::Host - + wasi::cli_base::exit::Host - + wasi::cli_base::preopens::Host - + wasi::filesystem::filesystem::Host - + wasi::sync_io::poll::poll::Host - + wasi::random::random::Host - + wasi::io::streams::Host - + wasi::clocks::monotonic_clock::Host - + wasi::clocks::wall_clock::Host, + + bindings::cli_base::environment::Host + + bindings::cli_base::exit::Host + + bindings::cli_base::preopens::Host + + bindings::filesystem::filesystem::Host + + bindings::sync_io::poll::poll::Host + + bindings::random::random::Host + + bindings::io::streams::Host + + bindings::clocks::monotonic_clock::Host + + bindings::clocks::wall_clock::Host, >( linker: &mut wasmtime::Linker, ) -> anyhow::Result<()> { @@ -617,15 +617,15 @@ fn first_non_empty_iovec<'a>( // stored in the WasiPreview1Adapter struct. impl< T: WasiPreview1View - + wasi::cli_base::environment::Host - + wasi::cli_base::exit::Host - + wasi::cli_base::preopens::Host - + wasi::filesystem::filesystem::Host - + wasi::sync_io::poll::poll::Host - + wasi::random::random::Host - + wasi::io::streams::Host - + wasi::clocks::monotonic_clock::Host - + wasi::clocks::wall_clock::Host, + + bindings::cli_base::environment::Host + + bindings::cli_base::exit::Host + + bindings::cli_base::preopens::Host + + bindings::filesystem::filesystem::Host + + bindings::sync_io::poll::poll::Host + + bindings::random::random::Host + + bindings::io::streams::Host + + bindings::clocks::monotonic_clock::Host + + bindings::clocks::wall_clock::Host, > wasi_snapshot_preview1::WasiSnapshotPreview1 for T { #[instrument(skip(self))] diff --git a/crates/wasi/src/preview2/preview2/clocks.rs b/crates/wasi/src/preview2/preview2/clocks.rs index a2aa059c6d7b..99407cc41d6f 100644 --- a/crates/wasi/src/preview2/preview2/clocks.rs +++ b/crates/wasi/src/preview2/preview2/clocks.rs @@ -1,6 +1,6 @@ #![allow(unused_variables)] -use crate::preview2::wasi::{ +use crate::preview2::bindings::{ clocks::monotonic_clock::{self, Instant}, clocks::timezone::{self, Timezone, TimezoneDisplay}, clocks::wall_clock::{self, Datetime}, diff --git a/crates/wasi/src/preview2/preview2/env.rs b/crates/wasi/src/preview2/preview2/env.rs index 8e6d946844c1..329c0dd08fe1 100644 --- a/crates/wasi/src/preview2/preview2/env.rs +++ b/crates/wasi/src/preview2/preview2/env.rs @@ -1,10 +1,6 @@ -use crate::preview2::wasi::cli_base::environment; -use crate::preview2::wasi::cli_base::preopens; -use crate::preview2::wasi::cli_base::stderr; -use crate::preview2::wasi::cli_base::stdin; -use crate::preview2::wasi::cli_base::stdout; -use crate::preview2::wasi::filesystem::filesystem; -use crate::preview2::wasi::io::streams; +use crate::preview2::bindings::cli_base::{environment, preopens, stderr, stdin, stdout}; +use crate::preview2::bindings::filesystem::filesystem; +use crate::preview2::bindings::io::streams; use crate::preview2::WasiView; impl environment::Host for T { diff --git a/crates/wasi/src/preview2/preview2/exit.rs b/crates/wasi/src/preview2/preview2/exit.rs index 6f008daadecb..24acff103b1e 100644 --- a/crates/wasi/src/preview2/preview2/exit.rs +++ b/crates/wasi/src/preview2/preview2/exit.rs @@ -1,4 +1,4 @@ -use crate::preview2::{wasi::cli_base::exit, I32Exit, WasiView}; +use crate::preview2::{bindings::cli_base::exit, I32Exit, WasiView}; impl exit::Host for T { fn exit(&mut self, status: Result<(), ()>) -> anyhow::Result<()> { diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 7d1caa85c623..12d794b998f3 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -1,9 +1,8 @@ +use crate::preview2::bindings::clocks::wall_clock; +use crate::preview2::bindings::filesystem::filesystem; +use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::{Dir, File, TableFsExt}; -use crate::preview2::stream::TableStreamExt; -use crate::preview2::wasi::clocks::wall_clock; -use crate::preview2::wasi::filesystem::filesystem; -use crate::preview2::wasi::io::streams; -use crate::preview2::{DirPerms, FilePerms, Table, TableError, WasiView}; +use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView}; use filesystem::ErrorCode; diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index f4e41324bec6..649f785d422f 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -1,8 +1,8 @@ use crate::preview2::{ + bindings::io::streams::{self, InputStream, OutputStream, StreamError}, + bindings::poll::poll::Pollable, poll::PollableFuture, stream::{HostInputStream, HostOutputStream, TableStreamExt}, - wasi::io::streams::{self, InputStream, OutputStream, StreamError}, - wasi::poll::poll::Pollable, HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -257,10 +257,10 @@ impl streams::Host for T { pub mod sync { use crate::preview2::{ + bindings::io::streams::Host as AsyncHost, + bindings::sync_io::io::streams::{self, InputStream, OutputStream}, + bindings::sync_io::poll::poll::Pollable, poll::sync::block_on, - wasi::io::streams::Host as AsyncHost, - wasi::sync_io::io::streams::{self, InputStream, OutputStream}, - wasi::sync_io::poll::poll::Pollable, WasiView, }; diff --git a/crates/wasi/src/preview2/preview2/random.rs b/crates/wasi/src/preview2/preview2/random.rs index 03798d4f47f4..d2483b39f9b9 100644 --- a/crates/wasi/src/preview2/preview2/random.rs +++ b/crates/wasi/src/preview2/preview2/random.rs @@ -1,6 +1,4 @@ -use crate::preview2::wasi::random::insecure; -use crate::preview2::wasi::random::insecure_seed; -use crate::preview2::wasi::random::random; +use crate::preview2::bindings::random::{insecure, insecure_seed, random}; use crate::preview2::WasiView; use cap_rand::{distributions::Standard, Rng}; diff --git a/crates/wasi/src/preview2/wasi/command.rs b/crates/wasi/src/preview2/wasi/command.rs deleted file mode 100644 index 37b364969a48..000000000000 --- a/crates/wasi/src/preview2/wasi/command.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::preview2::WasiView; - -wasmtime::component::bindgen!({ - world: "wasi:preview/command", - tracing: true, - async: true, - trappable_error_type: { - "filesystem"::"error-code": Error, - "streams"::"stream-error": Error, - }, - with: { - "wasi:filesystem/filesystem": crate::preview2::wasi::filesystem::filesystem, - "wasi:clocks/monotonic_clock": crate::preview2::wasi::clocks::monotonic_clock, - "wasi:poll/poll": crate::preview2::wasi::poll::poll, - "wasi:io/streams": crate::preview2::wasi::io::streams, - "wasi:clocks/timezone": crate::preview2::wasi::clocks::timezone, - "wasi:clocks/wall_clock": crate::preview2::wasi::clocks::wall_clock, - "wasi:random/random": crate::preview2::wasi::random::random, - "wasi:cli_base/environment": crate::preview2::wasi::cli_base::environment, - "wasi:cli_base/exit": crate::preview2::wasi::cli_base::exit, - "wasi:cli_base/preopens": crate::preview2::wasi::cli_base::preopens, - "wasi:cli_base/stdin": crate::preview2::wasi::cli_base::stdin, - "wasi:cli_base/stdout": crate::preview2::wasi::cli_base::stdout, - "wasi:cli_base/stderr": crate::preview2::wasi::cli_base::stderr, - }, -}); - -pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> anyhow::Result<()> { - crate::preview2::wasi::clocks::wall_clock::add_to_linker(l, |t| t)?; - crate::preview2::wasi::clocks::monotonic_clock::add_to_linker(l, |t| t)?; - crate::preview2::wasi::clocks::timezone::add_to_linker(l, |t| t)?; - crate::preview2::wasi::filesystem::filesystem::add_to_linker(l, |t| t)?; - crate::preview2::wasi::poll::poll::add_to_linker(l, |t| t)?; - crate::preview2::wasi::io::streams::add_to_linker(l, |t| t)?; - crate::preview2::wasi::random::random::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::exit::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::environment::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::preopens::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stdin::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stdout::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stderr::add_to_linker(l, |t| t)?; - Ok(()) -} - -pub mod sync { - use crate::preview2::WasiView; - - wasmtime::component::bindgen!({ - world: "wasi:preview/command", - tracing: true, - async: false, - trappable_error_type: { - "filesystem"::"error-code": Error, - "streams"::"stream-error": Error, - }, - with: { - "wasi:filesystem/filesystem": crate::preview2::wasi::filesystem::filesystem, - "wasi:clocks/monotonic_clock": crate::preview2::wasi::clocks::monotonic_clock, - "wasi:poll/poll": crate::preview2::wasi::sync_io::poll::poll, - "wasi:io/streams": crate::preview2::wasi::sync_io::io::streams, - "wasi:clocks/timezone": crate::preview2::wasi::clocks::timezone, - "wasi:clocks/wall_clock": crate::preview2::wasi::clocks::wall_clock, - "wasi:random/random": crate::preview2::wasi::random::random, - "wasi:cli_base/environment": crate::preview2::wasi::cli_base::environment, - "wasi:cli_base/exit": crate::preview2::wasi::cli_base::exit, - "wasi:cli_base/preopens": crate::preview2::wasi::cli_base::preopens, - "wasi:cli_base/stdin": crate::preview2::wasi::cli_base::stdin, - "wasi:cli_base/stdout": crate::preview2::wasi::cli_base::stdout, - "wasi:cli_base/stderr": crate::preview2::wasi::cli_base::stderr, - }, - }); - - pub fn add_to_linker( - l: &mut wasmtime::component::Linker, - ) -> anyhow::Result<()> { - crate::preview2::wasi::clocks::wall_clock::add_to_linker(l, |t| t)?; - crate::preview2::wasi::clocks::monotonic_clock::add_to_linker(l, |t| t)?; - crate::preview2::wasi::clocks::timezone::add_to_linker(l, |t| t)?; - crate::preview2::wasi::filesystem::filesystem::add_to_linker(l, |t| t)?; - crate::preview2::wasi::sync_io::poll::poll::add_to_linker(l, |t| t)?; - crate::preview2::wasi::sync_io::io::streams::add_to_linker(l, |t| t)?; - crate::preview2::wasi::random::random::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::exit::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::environment::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::preopens::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stdin::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stdout::add_to_linker(l, |t| t)?; - crate::preview2::wasi::cli_base::stderr::add_to_linker(l, |t| t)?; - Ok(()) - } -} diff --git a/crates/wasi/src/preview2/wasi/mod.rs b/crates/wasi/src/preview2/wasi/mod.rs deleted file mode 100644 index 28306c237003..000000000000 --- a/crates/wasi/src/preview2/wasi/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -pub mod command; - -pub mod sync_io { - pub(crate) mod _internal { - wasmtime::component::bindgen!({ - path: "wit", - interfaces: " - import wasi:poll/poll - import wasi:io/streams - ", - tracing: true, - trappable_error_type: { - "streams"::"stream-error": Error, - } - }); - } - pub use self::_internal::wasi::{io, poll}; - - impl From for io::streams::StreamError { - fn from(_other: super::io::streams::StreamError) -> Self { - // There are no cases for this record. - Self {} - } - } - - impl From for io::streams::Error { - fn from(other: super::io::streams::Error) -> Self { - match other.downcast() { - Ok(se) => io::streams::Error::from(io::streams::StreamError::from(se)), - Err(e) => io::streams::Error::trap(e), - } - } - } -} - -pub(crate) mod _internal_io { - wasmtime::component::bindgen!({ - path: "wit", - interfaces: " - import wasi:poll/poll - import wasi:io/streams - ", - tracing: true, - async: true, - trappable_error_type: { - "streams"::"stream-error": Error, - } - }); -} -pub use self::_internal_io::wasi::{io, poll}; -pub(crate) mod _internal_rest { - wasmtime::component::bindgen!({ - path: "wit", - interfaces: " - import wasi:clocks/wall-clock - import wasi:clocks/monotonic-clock - import wasi:clocks/timezone - import wasi:filesystem/filesystem - import wasi:random/random - import wasi:random/insecure - import wasi:random/insecure-seed - import wasi:cli-base/environment - import wasi:cli-base/preopens - import wasi:cli-base/exit - import wasi:cli-base/stdin - import wasi:cli-base/stdout - import wasi:cli-base/stderr - ", - tracing: true, - trappable_error_type: { - "filesystem"::"error-code": Error, - "streams"::"stream-error": Error, - }, - with: { - "wasi:poll/poll": crate::preview2::wasi::poll::poll, - "wasi:io/streams": crate::preview2::wasi::io::streams - } - }); -} -pub use self::_internal_rest::wasi::*; From 6994d6cd663663fba54148cc485a372496388064 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 08:54:36 -0700 Subject: [PATCH 048/118] fix renaming of wasi to bindings in tests --- crates/test-programs/tests/command.rs | 5 +-- crates/test-programs/tests/reactor.rs | 36 +++++++++---------- .../tests/wasi-preview2-components-sync.rs | 2 +- .../tests/wasi-preview2-components.rs | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index 3867c647368e..c176deaaf43b 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -6,8 +6,9 @@ use wasmtime::{ Config, Engine, Store, }; use wasmtime_wasi::preview2::{ - pipe::MemoryInputPipe, wasi::command::add_to_linker, wasi::command::Command, DirPerms, - FilePerms, Table, WasiClocks, WasiCtx, WasiCtxBuilder, WasiMonotonicClock, WasiView, + command::{add_to_linker, Command}, + pipe::MemoryInputPipe, + DirPerms, FilePerms, Table, WasiClocks, WasiCtx, WasiCtxBuilder, WasiMonotonicClock, WasiView, WasiWallClock, }; diff --git a/crates/test-programs/tests/reactor.rs b/crates/test-programs/tests/reactor.rs index d58b6cf996f1..ad5ab5213a6a 100644 --- a/crates/test-programs/tests/reactor.rs +++ b/crates/test-programs/tests/reactor.rs @@ -3,8 +3,8 @@ use wasmtime::{ component::{Component, Linker}, Config, Engine, Store, }; -use wasmtime_wasi::preview2::wasi::clocks::wall_clock; -use wasmtime_wasi::preview2::wasi::filesystem::filesystem; +use wasmtime_wasi::preview2::bindings::clocks::wall_clock; +use wasmtime_wasi::preview2::bindings::filesystem::filesystem; use wasmtime_wasi::preview2::{self, Table, WasiCtx, WasiCtxBuilder, WasiView}; lazy_static::lazy_static! { @@ -27,14 +27,14 @@ wasmtime::component::bindgen!({ world: "test-reactor", async: true, with: { - "wasi:io/streams": preview2::wasi::io::streams, - "wasi:filesystem/filesystem": preview2::wasi::filesystem::filesystem, - "wasi:cli-base/environment": preview2::wasi::cli_base::environment, - "wasi:cli-base/preopens": preview2::wasi::cli_base::preopens, - "wasi:cli-base/exit": preview2::wasi::cli_base::exit, - "wasi:cli-base/stdin": preview2::wasi::cli_base::stdin, - "wasi:cli-base/stdout": preview2::wasi::cli_base::stdout, - "wasi:cli-base/stderr": preview2::wasi::cli_base::stderr, + "wasi:io/streams": preview2::bindings::io::streams, + "wasi:filesystem/filesystem": preview2::bindings::filesystem::filesystem, + "wasi:cli-base/environment": preview2::bindings::cli_base::environment, + "wasi:cli-base/preopens": preview2::bindings::cli_base::preopens, + "wasi:cli-base/exit": preview2::bindings::cli_base::exit, + "wasi:cli-base/stdin": preview2::bindings::cli_base::stdin, + "wasi:cli-base/stdout": preview2::bindings::cli_base::stdout, + "wasi:cli-base/stderr": preview2::bindings::cli_base::stderr, }, ownership: Borrowing { duplicate_if_necessary: false @@ -68,14 +68,14 @@ async fn instantiate( let mut linker = Linker::new(&ENGINE); // All of the imports available to the world are provided by the wasi-common crate: - preview2::wasi::filesystem::filesystem::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::io::streams::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::environment::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::preopens::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::exit::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::stdin::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::stdout::add_to_linker(&mut linker, |x| x)?; - preview2::wasi::cli_base::stderr::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::filesystem::filesystem::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::io::streams::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::environment::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::preopens::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::exit::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::stdin::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::stdout::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli_base::stderr::add_to_linker(&mut linker, |x| x)?; let mut store = Store::new(&ENGINE, wasi_ctx); diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index a7959153d5b1..80a0ec4aa006 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -3,8 +3,8 @@ use anyhow::Result; use tempfile::TempDir; use wasmtime::{component::Linker, Config, Engine, Store}; use wasmtime_wasi::preview2::{ + command::sync::{add_to_linker, Command}, pipe::MemoryOutputPipe, - wasi::command::sync::{add_to_linker, Command}, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, }; diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index 26e73e7ad758..cf59ca5ef931 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -3,8 +3,8 @@ use anyhow::Result; use tempfile::TempDir; use wasmtime::{component::Linker, Config, Engine, Store}; use wasmtime_wasi::preview2::{ + command::{add_to_linker, Command}, pipe::MemoryOutputPipe, - wasi::command::{add_to_linker, Command}, DirPerms, FilePerms, Table, WasiCtx, WasiCtxBuilder, WasiView, }; From 3f7561fa73be697b4ae23f419ed294c47fabcf2e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 13:11:30 -0700 Subject: [PATCH 049/118] use block_in_place throughout filesystem and move block_on and block_in_place to be pub crate at the root --- crates/wasi/src/preview2/filesystem.rs | 24 ++-- crates/wasi/src/preview2/mod.rs | 33 ++++++ crates/wasi/src/preview2/poll.rs | 25 +--- .../wasi/src/preview2/preview2/filesystem.rs | 112 ++++++++++-------- crates/wasi/src/preview2/preview2/io.rs | 3 +- crates/wasi/src/preview2/stdio/unix.rs | 2 +- 6 files changed, 107 insertions(+), 92 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 497c6168f221..96a2813b3258 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,4 +1,6 @@ -use crate::preview2::{HostInputStream, HostOutputStream, StreamState, Table, TableError}; +use crate::preview2::{ + block_in_place, HostInputStream, HostOutputStream, StreamState, Table, TableError, +}; use std::sync::Arc; bitflags::bitflags! { @@ -98,8 +100,7 @@ impl FileInputStream { impl HostInputStream for FileInputStream { fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - let (n, end) = read_result(self.file.read_at(buf, self.position))?; + let (n, end) = read_result(block_in_place(|| self.file.read_at(buf, self.position)))?; self.position = self.position.wrapping_add(n); Ok((n, end)) } @@ -108,8 +109,9 @@ impl HostInputStream for FileInputStream { bufs: &mut [std::io::IoSliceMut<'a>], ) -> anyhow::Result<(u64, StreamState)> { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - let (n, end) = read_result(self.file.read_vectored_at(bufs, self.position))?; + let (n, end) = read_result(block_in_place(|| { + self.file.read_vectored_at(bufs, self.position) + }))?; self.position = self.position.wrapping_add(n); Ok((n, end)) } @@ -148,8 +150,7 @@ impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - let n = self.file.write_at(buf, self.position)? as i64 as u64; + let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; self.position = self.position.wrapping_add(n); Ok(n) } @@ -157,8 +158,7 @@ impl HostOutputStream for FileOutputStream { /// Vectored-I/O form of `write`. fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - let n = self.file.write_vectored_at(bufs, self.position)? as i64 as u64; + let n = block_in_place(|| self.file.write_vectored_at(bufs, self.position))? as i64 as u64; self.position = self.position.wrapping_add(n); Ok(n) } @@ -189,15 +189,13 @@ impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: &[u8]) -> anyhow::Result { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - Ok(self.file.append(buf)? as i64 as u64) + Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) } /// Vectored-I/O form of `write`. fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { use system_interface::fs::FileIoExt; - // FIXME spawn_blocking to put this on a tokio worker thread - let n = self.file.append_vectored(bufs)? as i64 as u64; + let n = block_in_place(|| self.file.append_vectored(bufs))? as i64 as u64; Ok(n) } diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 2b15c1d032e9..8cee30a92fe1 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -126,3 +126,36 @@ pub mod bindings { } pub use self::_internal_rest::wasi::*; } + +pub(crate) fn block_on(f: F) -> F::Output { + use tokio::runtime::{Builder, Handle, Runtime}; + match Handle::try_current() { + Ok(h) => { + let _enter = h.enter(); + h.block_on(f) + } + Err(_) => { + use once_cell::sync::Lazy; + static RUNTIME: Lazy = Lazy::new(|| { + Builder::new_current_thread() + .enable_time() + .enable_io() + .build() + .unwrap() + }); + let _enter = RUNTIME.enter(); + RUNTIME.block_on(f) + } + } +} + +pub(crate) fn block_in_place(f: F) -> R +where + F: FnOnce() -> R, +{ + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(f) + } else { + f() + } +} diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 314c133b407e..a48c8bba70bd 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -117,32 +117,9 @@ pub mod sync { use crate::preview2::{ bindings::poll::poll::Host as AsyncHost, bindings::sync_io::poll::poll::{self, Pollable}, - WasiView, + block_on, WasiView, }; use anyhow::Result; - use std::future::Future; - use tokio::runtime::{Builder, Handle, Runtime}; - - pub fn block_on(f: F) -> F::Output { - match Handle::try_current() { - Ok(h) => { - let _enter = h.enter(); - h.block_on(f) - } - Err(_) => { - use once_cell::sync::Lazy; - static RUNTIME: Lazy = Lazy::new(|| { - Builder::new_current_thread() - .enable_time() - .enable_io() - .build() - .unwrap() - }); - let _enter = RUNTIME.enter(); - RUNTIME.block_on(f) - } - } - } impl poll::Host for T { fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 12d794b998f3..65f0ed313a81 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -2,7 +2,9 @@ use crate::preview2::bindings::clocks::wall_clock; use crate::preview2::bindings::filesystem::filesystem; use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::{Dir, File, TableFsExt}; -use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView}; +use crate::preview2::{ + block_in_place, DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView, +}; use filesystem::ErrorCode; @@ -36,14 +38,15 @@ impl filesystem::Host for T { }; let f = self.table().get_file(fd)?; - f.file.advise(offset, len, advice)?; + block_in_place(|| f.file.advise(offset, len, advice))?; Ok(()) } fn sync_data(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { - match table.get_file(fd)?.file.sync_data() { + let f = table.get_file(fd)?; + match block_in_place(|| f.file.sync_data()) { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -58,11 +61,8 @@ impl filesystem::Host for T { Err(e) => Err(e.into()), } } else if table.is_dir(fd) { - Ok(table - .get_dir(fd)? - .dir - .open(std::path::Component::CurDir)? - .sync_data()?) + let d = table.get_dir(fd)?; + block_in_place(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_data()?)) } else { Err(ErrorCode::BadDescriptor.into()) } @@ -77,7 +77,8 @@ impl filesystem::Host for T { use system_interface::fs::{FdFlags, GetSetFdFlags}; fn get_from_fdflags(f: impl AsFilelike) -> std::io::Result { - let flags = f.as_filelike().get_fd_flags()?; + let f = f.as_filelike(); + let flags = block_in_place(|| f.get_fd_flags())?; let mut out = DescriptorFlags::empty(); if flags.contains(FdFlags::DSYNC) { out |= DescriptorFlags::REQUESTED_WRITE_SYNC; @@ -124,7 +125,8 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { - let meta = table.get_file(fd)?.file.metadata()?; + let f = table.get_file(fd)?; + let meta = block_in_place(|| f.file.metadata())?; Ok(descriptortype_from(meta.file_type())) } else if table.is_dir(fd) { Ok(filesystem::DescriptorType::Directory) @@ -142,7 +144,7 @@ impl filesystem::Host for T { if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; } - f.file.set_len(size)?; + block_in_place(|| f.file.set_len(size))?; Ok(()) } @@ -162,7 +164,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - f.file.set_times(atim, mtim)?; + block_in_place(|| f.file.set_times(atim, mtim))?; Ok(()) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; @@ -171,7 +173,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - d.dir.set_times(atim, mtim)?; + block_in_place(|| d.dir.set_times(atim, mtim))?; Ok(()) } else { Err(ErrorCode::BadDescriptor.into()) @@ -195,10 +197,10 @@ impl filesystem::Host for T { } let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; - let (bytes_read, state) = crate::preview2::filesystem::read_result( + let (bytes_read, state) = crate::preview2::filesystem::read_result(block_in_place(|| { f.file - .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset), - )?; + .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset) + }))?; buffer.truncate( bytes_read @@ -224,7 +226,8 @@ impl filesystem::Host for T { return Err(ErrorCode::NotPermitted.into()); } - let bytes_written = f.file.write_vectored_at(&[IoSlice::new(&buf)], offset)?; + let bytes_written = + block_in_place(|| f.file.write_vectored_at(&[IoSlice::new(&buf)], offset))?; Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) } @@ -251,9 +254,9 @@ impl filesystem::Host for T { } } - let entries = d.dir.entries()?.map(|entry| { + let entries = block_in_place(|| d.dir.entries())?.map(|entry| { let entry = entry?; - let meta = entry.full_metadata()?; + let meta = block_in_place(|| entry.full_metadata())?; let inode = Some(meta.ino()); let type_ = descriptortype_from(meta.file_type()); let name = entry @@ -304,7 +307,8 @@ impl filesystem::Host for T { fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { - match table.get_file(fd)?.file.sync_all() { + let f = table.get_file(fd)?; + match block_in_place(|| f.file.sync_all()) { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -319,11 +323,8 @@ impl filesystem::Host for T { Err(e) => Err(e.into()), } } else if table.is_dir(fd) { - Ok(table - .get_dir(fd)? - .dir - .open(std::path::Component::CurDir)? - .sync_all()?) + let d = table.get_dir(fd)?; + block_in_place(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_all()?)) } else { Err(ErrorCode::BadDescriptor.into()) } @@ -339,7 +340,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - d.dir.create_dir(&path)?; + block_in_place(|| d.dir.create_dir(&path))?; Ok(()) } @@ -351,12 +352,12 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = f.file.metadata()?; + let meta = block_in_place(|| f.file.metadata())?; Ok(descriptorstat_from(meta)) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = d.dir.dir_metadata()?; + let meta = block_in_place(|| d.dir.dir_metadata())?; Ok(descriptorstat_from(meta)) } else { Err(ErrorCode::BadDescriptor.into()) @@ -376,9 +377,9 @@ impl filesystem::Host for T { } let meta = if symlink_follow(path_flags) { - d.dir.metadata(&path)? + block_in_place(|| d.dir.metadata(&path))? } else { - d.dir.symlink_metadata(&path)? + block_in_place(|| d.dir.symlink_metadata(&path))? }; Ok(descriptorstat_from(meta)) } @@ -401,17 +402,21 @@ impl filesystem::Host for T { let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; if symlink_follow(path_flags) { - d.dir.set_times( - &path, - atim.map(cap_fs_ext::SystemTimeSpec::from_std), - mtim.map(cap_fs_ext::SystemTimeSpec::from_std), - )?; + block_in_place(|| { + d.dir.set_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + })?; } else { - d.dir.set_symlink_times( - &path, - atim.map(cap_fs_ext::SystemTimeSpec::from_std), - mtim.map(cap_fs_ext::SystemTimeSpec::from_std), - )?; + block_in_place(|| { + d.dir.set_symlink_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + })?; } Ok(()) } @@ -437,7 +442,7 @@ impl filesystem::Host for T { if symlink_follow(old_path_flags) { return Err(ErrorCode::Invalid.into()); } - old_dir.dir.hard_link(&old_path, &new_dir.dir, &new_path)?; + block_in_place(|| old_dir.dir.hard_link(&old_path, &new_dir.dir, &new_path))?; Ok(()) } @@ -519,9 +524,9 @@ impl filesystem::Host for T { Err(ErrorCode::Invalid)?; } } - let mut opened = d.dir.open_with(&path, &opts)?; + let mut opened = block_in_place(|| d.dir.open_with(&path, &opts))?; - if opened.metadata()?.is_dir() { + if block_in_place(|| opened.metadata())?.is_dir() { Ok(table.push_dir(Dir::new( cap_std::fs::Dir::from_std_file(opened.into_std()), d.perms, @@ -532,8 +537,11 @@ impl filesystem::Host for T { } else { // FIXME cap-std needs a nonblocking open option so that files reads and writes // are nonblocking. Instead we set it after opening here: - let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; - opened.set_fd_flags(set_fd_flags)?; + block_in_place(|| { + let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; + opened.set_fd_flags(set_fd_flags)?; + Ok::<(), filesystem::Error>(()) + })?; Ok(table.push_file(File::new(opened, mask_file_perms(d.file_perms, flags)))?) } @@ -541,8 +549,8 @@ impl filesystem::Host for T { fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { let table = self.table_mut(); - if table.delete_file(fd).is_err() { - table.delete_dir(fd)?; + if block_in_place(|| table.delete_file(fd)).is_err() { + block_in_place(|| table.delete_dir(fd))?; } Ok(()) } @@ -557,7 +565,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::READ) { return Err(ErrorCode::NotPermitted.into()); } - let link = d.dir.read_link(&path)?; + let link = block_in_place(|| d.dir.read_link(&path))?; Ok(link .into_os_string() .into_string() @@ -574,7 +582,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.dir.remove_dir(&path)?) + Ok(block_in_place(|| d.dir.remove_dir(&path))?) } fn rename_at( @@ -593,7 +601,7 @@ impl filesystem::Host for T { if !new_dir.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - old_dir.dir.rename(&old_path, &new_dir.dir, &new_path)?; + block_in_place(|| old_dir.dir.rename(&old_path, &new_dir.dir, &new_path))?; Ok(()) } @@ -612,7 +620,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - d.dir.symlink(&src_path, &dest_path)?; + block_in_place(|| d.dir.symlink(&src_path, &dest_path))?; Ok(()) } @@ -628,7 +636,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - d.dir.remove_file_or_symlink(&path)?; + block_in_place(|| d.dir.remove_file_or_symlink(&path))?; Ok(()) } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 649f785d422f..667c7254027c 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -260,8 +260,7 @@ pub mod sync { bindings::io::streams::Host as AsyncHost, bindings::sync_io::io::streams::{self, InputStream, OutputStream}, bindings::sync_io::poll::poll::Pollable, - poll::sync::block_on, - WasiView, + block_on, WasiView, }; impl streams::Host for T { diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index dc9c4e7c845d..bd87c2f7e5fa 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -28,7 +28,7 @@ impl Stdin { create().expect("creating AsyncFd for stdin in existing tokio context") }), Err(_) => STDIN.get_or_init(|| { - crate::preview2::poll::sync::block_on(async { + crate::preview2::block_on(async { create().expect("creating AsyncFd for stdin in internal tokio context") }) }), From 789beec9f23a651fe8e59837759817c7e61a8d2d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 14:03:03 -0700 Subject: [PATCH 050/118] AsyncFdStream: ensure file is nonblocking --- crates/wasi/Cargo.toml | 3 +-- crates/wasi/src/preview2/stream.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index f7eb80253db9..5ee6b3e52411 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -33,11 +33,10 @@ fs-set-times = { workspace = true, optional = true } bitflags = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } system-interface = { workspace = true, optional = true} -rustix = { workspace = true, features = ["net"], optional = true} futures-util = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] -rustix = { workspace = true, features = ["fs"] } +rustix = { workspace = true, features = ["fs"], optional = true } [target.'cfg(windows)'.dependencies] io-extras = "0.17.1" diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index ccd43ab50abd..f7b4aae65492 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -342,7 +342,7 @@ pub use async_fd_stream::*; #[cfg(unix)] mod async_fd_stream { use super::{HostInputStream, HostOutputStream, StreamState}; - use anyhow::Error; + use anyhow::{bail, Error}; use std::io::{Read, Write}; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use tokio::io::unix::AsyncFd; @@ -353,8 +353,12 @@ mod async_fd_stream { impl AsyncFdStream { pub fn new(fd: T) -> anyhow::Result { - // FIXME: Use fcntl to make sure O_NONBLOCKING is set for this fd, - // otherwise the implementations below will not be correct. + let flags = rustix::fs::fcntl_getfl(unsafe { + rustix::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) + })?; + if !flags.contains(rustix::fs::OFlags::NONBLOCK) { + bail!("AsyncFdStream::new requires a nonblocking file"); + } Ok(Self { fd: AsyncFd::new(fd)?, }) From 9da134bea48d35305d2e5e410b31e79e705e5fff Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 14:04:47 -0700 Subject: [PATCH 051/118] tests: block_in_place requires multi-threaded runtime --- crates/test-programs/tests/command.rs | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index c176deaaf43b..68b955685056 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -59,7 +59,7 @@ async fn instantiate( Ok((store, command)) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn hello_stdout() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -73,7 +73,7 @@ async fn hello_stdout() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn panic() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -96,7 +96,7 @@ async fn panic() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn args() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -110,7 +110,7 @@ async fn args() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn random() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -123,7 +123,7 @@ async fn random() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn time() -> Result<()> { struct FakeWallClock; @@ -171,7 +171,7 @@ async fn time() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -187,7 +187,7 @@ async fn stdin() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn poll_stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -203,7 +203,7 @@ async fn poll_stdin() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn env() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() @@ -220,7 +220,7 @@ async fn env() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_read() -> Result<()> { let dir = tempfile::tempdir()?; @@ -242,7 +242,7 @@ async fn file_read() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_append() -> Result<()> { let dir = tempfile::tempdir()?; @@ -274,7 +274,7 @@ async fn file_append() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_dir_sync() -> Result<()> { let dir = tempfile::tempdir()?; @@ -297,7 +297,7 @@ async fn file_dir_sync() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn exit_success() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -314,7 +314,7 @@ async fn exit_success() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn exit_default() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -327,7 +327,7 @@ async fn exit_default() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn exit_failure() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -344,7 +344,7 @@ async fn exit_failure() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn exit_panic() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -361,7 +361,7 @@ async fn exit_panic() -> Result<()> { Ok(()) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn directory_list() -> Result<()> { let dir = tempfile::tempdir()?; @@ -389,7 +389,7 @@ async fn directory_list() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn default_clocks() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -403,7 +403,7 @@ async fn default_clocks() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn export_cabi_realloc() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new().build(&mut table)?; @@ -419,7 +419,7 @@ async fn export_cabi_realloc() -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } -#[test_log::test(tokio::test)] +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn read_only() -> Result<()> { let dir = tempfile::tempdir()?; From 783966fdc4aa4f4c28704a4ef05cf3bda339c919 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 14:14:37 -0700 Subject: [PATCH 052/118] actually, use fcntl_setfl to make the asyncfd file nonblocking --- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/stream.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 8cee30a92fe1..31feaad1b60c 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -137,7 +137,7 @@ pub(crate) fn block_on(f: F) -> F::Output { Err(_) => { use once_cell::sync::Lazy; static RUNTIME: Lazy = Lazy::new(|| { - Builder::new_current_thread() + Builder::new_multi_thread() .enable_time() .enable_io() .build() diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index f7b4aae65492..4738c629b2e0 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -342,7 +342,7 @@ pub use async_fd_stream::*; #[cfg(unix)] mod async_fd_stream { use super::{HostInputStream, HostOutputStream, StreamState}; - use anyhow::{bail, Error}; + use anyhow::Error; use std::io::{Read, Write}; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use tokio::io::unix::AsyncFd; @@ -353,12 +353,15 @@ mod async_fd_stream { impl AsyncFdStream { pub fn new(fd: T) -> anyhow::Result { - let flags = rustix::fs::fcntl_getfl(unsafe { - rustix::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) + use rustix::fs::OFlags; + let borrowed_fd = unsafe { rustix::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) }; + tokio::task::block_in_place(|| { + let flags = rustix::fs::fcntl_getfl(borrowed_fd)?; + if !flags.contains(OFlags::NONBLOCK) { + rustix::fs::fcntl_setfl(borrowed_fd, flags.difference(OFlags::NONBLOCK))?; + } + Ok::<(), anyhow::Error>(()) })?; - if !flags.contains(rustix::fs::OFlags::NONBLOCK) { - bail!("AsyncFdStream::new requires a nonblocking file"); - } Ok(Self { fd: AsyncFd::new(fd)?, }) From a6999815b748ea2910533dc4b68a46531a8dc350 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 15:24:35 -0700 Subject: [PATCH 053/118] fix windows block_on --- crates/wasi/src/preview2/stdio/windows.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/wasi/src/preview2/stdio/windows.rs b/crates/wasi/src/preview2/stdio/windows.rs index b57e7c7b4634..e24cdf4b3e4e 100644 --- a/crates/wasi/src/preview2/stdio/windows.rs +++ b/crates/wasi/src/preview2/stdio/windows.rs @@ -49,9 +49,7 @@ impl Stdin { // Creation must be running in a tokio context to succeed. match tokio::runtime::Handle::try_current() { Ok(_) => STDIN.get_or_init(|| create()), - Err(_) => { - STDIN.get_or_init(|| crate::preview2::poll::sync::block_on(async { create() })) - } + Err(_) => STDIN.get_or_init(|| crate::preview2::block_on(async { create() })), } } } From 1f39f9d873a1582ff6f12b99caa4505f2d6f859c Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 15:33:22 -0700 Subject: [PATCH 054/118] docs, remove unnecessary methods --- crates/wasi/src/preview2/preview2/io.rs | 6 +++-- crates/wasi/src/preview2/stream.rs | 35 +++++++++---------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 667c7254027c..31913cd3c40d 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -213,7 +213,8 @@ impl streams::Host for T { } async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { - let _ = self.table().get_input_stream(stream)?; + // Ensure that table element is an input-stream: + let _ = self.table_mut().get_input_stream_mut(stream)?; fn input_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { let stream = stream @@ -235,7 +236,8 @@ impl streams::Host for T { &mut self, stream: OutputStream, ) -> anyhow::Result { - let _ = self.table().get_output_stream(stream)?; + // Ensure that table element is an output-stream: + let _ = self.table_mut().get_output_stream_mut(stream)?; fn output_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { let stream = stream diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 4738c629b2e0..17915db4e7f0 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -15,11 +15,8 @@ impl StreamState { } } -/// An input bytestream. -/// -/// This is "pseudo" because the real streams will be a type in wit, and -/// built into the wit bindings, and will support async and type parameters. -/// This pseudo-stream abstraction is synchronous and only supports bytes. +/// Host trait for implementing the `wasi:io/streams.input-stream` resource: A +/// bytestream which can be read from. #[async_trait::async_trait] pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes read @@ -66,11 +63,8 @@ pub trait HostInputStream: Send + Sync { async fn ready(&mut self) -> Result<(), Error>; } -/// An output bytestream. -/// -/// This is "pseudo" because the real streams will be a type in wit, and -/// built into the wit bindings, and will support async and type parameters. -/// This pseudo-stream abstraction is synchronous and only supports bytes. +/// Host trait for implementing the `wasi:io/streams.output-stream` resource: +/// A bytestream which can be written to. #[async_trait::async_trait] pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. @@ -137,7 +131,6 @@ pub trait HostOutputStream: Send + Sync { pub trait TableStreamExt { fn push_input_stream(&mut self, istream: Box) -> Result; - fn get_input_stream(&self, fd: u32) -> Result<&dyn HostInputStream, TableError>; fn get_input_stream_mut( &mut self, fd: u32, @@ -145,7 +138,6 @@ pub trait TableStreamExt { fn push_output_stream(&mut self, ostream: Box) -> Result; - fn get_output_stream(&self, fd: u32) -> Result<&dyn HostOutputStream, TableError>; fn get_output_stream_mut( &mut self, fd: u32, @@ -155,9 +147,6 @@ impl TableStreamExt for Table { fn push_input_stream(&mut self, istream: Box) -> Result { self.push(Box::new(istream)) } - fn get_input_stream(&self, fd: u32) -> Result<&dyn HostInputStream, TableError> { - self.get::>(fd).map(|f| f.as_ref()) - } fn get_input_stream_mut( &mut self, fd: u32, @@ -171,10 +160,6 @@ impl TableStreamExt for Table { ) -> Result { self.push(Box::new(ostream)) } - fn get_output_stream(&self, fd: u32) -> Result<&dyn HostOutputStream, TableError> { - self.get::>(fd) - .map(|f| f.as_ref()) - } fn get_output_stream_mut( &mut self, fd: u32, @@ -183,6 +168,7 @@ impl TableStreamExt for Table { } } +/// Provides a [`HostInputStream`] impl from a [`tokio::io::AsyncRead`] impl pub struct AsyncReadStream { state: StreamState, buffer: Vec, @@ -266,6 +252,7 @@ impl HostInputStream } } +/// Provides a [`HostOutputStream`] impl from a [`tokio::io::AsyncWrite`] impl pub struct AsyncWriteStream { buffer: Vec, writer: T, @@ -347,6 +334,8 @@ mod async_fd_stream { use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use tokio::io::unix::AsyncFd; + /// Provides a [`HostInputStream`] and [`HostOutputStream`] impl from an + /// [`std::os::fd::AsRawFd`] impl, using [`tokio::io::unix::AsyncFd`] pub struct AsyncFdStream { fd: AsyncFd, } @@ -440,9 +429,9 @@ mod test { let dummy = DummyInputStream; let mut table = Table::new(); - // Show that we can put an input stream in the table, and both retrieval functions work: + // Show that we can put an input stream in the table, and get a mut + // ref back out: let ix = table.push_input_stream(Box::new(dummy)).unwrap(); - let _ = table.get_input_stream(ix).unwrap(); let _ = table.get_input_stream_mut(ix).unwrap(); } @@ -461,9 +450,9 @@ mod test { let dummy = DummyOutputStream; let mut table = Table::new(); - // Show that we can put an output stream in the table, and both retrieval functions work: + // Show that we can put an output stream in the table, and get a mut + // ref back out: let ix = table.push_output_stream(Box::new(dummy)).unwrap(); - let _ = table.get_output_stream(ix).unwrap(); let _ = table.get_output_stream_mut(ix).unwrap(); } } From 22f340a3242ac5a0f4a15b4d199c66d6b3886329 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 29 Jun 2023 15:59:07 -0700 Subject: [PATCH 055/118] more docs --- crates/wasi/src/preview2/poll.rs | 12 ++++++++ crates/wasi/src/preview2/stream.rs | 44 ++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index a48c8bba70bd..4281820b266d 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -13,8 +13,20 @@ pub type PollableFuture<'a> = Pin> + Send + ' pub type MakeFuture = for<'a> fn(&'a mut dyn Any) -> PollableFuture<'a>; pub type ClosureFuture = Box PollableFuture<'static> + Send + Sync + 'static>; +/// A host representation of the `wasi:poll/poll.pollable` resource. +/// +/// A pollable is not the same thing as a Rust Future: the same pollable may be used to +/// repeatedly check for readiness of a given condition, e.g. if a stream is readable +/// or writable. So, rather than containing a Future, which can only become Ready once, a +/// HostPollable contains a way to create a Future in each call to poll_oneoff. pub enum HostPollable { + /// Create a Future by calling a fn on another resource in the table. This + /// indirection means the created Future can use a mut borrow of another + /// resource in the Table (e.g. a stream) TableEntry { index: u32, make_future: MakeFuture }, + /// Create a future by calling an owned, static closure. This is used for + /// pollables which do not share state with another resource in the Table + /// (e.g. a timer) Closure(ClosureFuture), } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 17915db4e7f0..6a370615a905 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -19,11 +19,13 @@ impl StreamState { /// bytestream which can be read from. #[async_trait::async_trait] pub trait HostInputStream: Send + Sync { - /// Read bytes. On success, returns a pair holding the number of bytes read - /// and a flag indicating whether the end of the stream was reached. + /// Read bytes. On success, returns a pair holding the number of bytes + /// read and a flag indicating whether the end of the stream was reached. + /// Important: this read must be non-blocking! fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; - /// Vectored-I/O form of `read`. + /// Vectored-I/O form of `read`. Important: this read must be + /// non-blocking! fn read_vectored<'a>( &mut self, bufs: &mut [std::io::IoSliceMut<'a>], @@ -41,7 +43,8 @@ pub trait HostInputStream: Send + Sync { false } - /// Read bytes from a stream and discard them. + /// Read bytes from a stream and discard them. Important: this method must + /// be non-blocking! fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { let mut nread = 0; let mut state = StreamState::Open; @@ -59,7 +62,8 @@ pub trait HostInputStream: Send + Sync { Ok((nread, state)) } - /// An async method to check read readiness. + /// Check for read readiness: this method blocks until the stream is ready + /// for reading. async fn ready(&mut self) -> Result<(), Error>; } @@ -68,9 +72,11 @@ pub trait HostInputStream: Send + Sync { #[async_trait::async_trait] pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. + /// Important: this write must be non-blocking! fn write(&mut self, _buf: &[u8]) -> Result; - /// Vectored-I/O form of `write`. + /// Vectored-I/O form of `write`. Important: this write must be + /// non-blocking! fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { if bufs.len() > 0 { self.write(bufs.get(0).unwrap()) @@ -86,6 +92,7 @@ pub trait HostOutputStream: Send + Sync { } /// Transfer bytes directly from an input stream to an output stream. + /// Important: this splice must be non-blocking! fn splice( &mut self, src: &mut dyn HostInputStream, @@ -109,7 +116,8 @@ pub trait HostOutputStream: Send + Sync { Ok((nspliced, state)) } - /// Repeatedly write a byte to a stream. + /// Repeatedly write a byte to a stream. Important: this write must be + /// non-blocking! fn write_zeroes(&mut self, nelem: u64) -> Result { let mut nwritten = 0; @@ -125,19 +133,25 @@ pub trait HostOutputStream: Send + Sync { Ok(nwritten) } - /// An async method to check write readiness. + /// Check for write readiness: this method blocks until the stream is + /// ready for writing. async fn ready(&mut self) -> Result<(), Error>; } +/// Extension trait for managing [`HostInputStream`]s and [`HostOutputStream`]s in the [`Table`]. pub trait TableStreamExt { + /// Push a [`HostInputStream`] into a [`Table`], returning the table index. fn push_input_stream(&mut self, istream: Box) -> Result; + /// Get a mutable reference to a [`HostInputStream`] in a [`Table`]. fn get_input_stream_mut( &mut self, fd: u32, ) -> Result<&mut Box, TableError>; + /// Push a [`HostOutputStream`] into a [`Table`], returning the table index. fn push_output_stream(&mut self, ostream: Box) -> Result; + /// Get a mutable reference to a [`HostOutputStream`] in a [`Table`]. fn get_output_stream_mut( &mut self, fd: u32, @@ -176,6 +190,8 @@ pub struct AsyncReadStream { } impl AsyncReadStream { + /// Create a [`AsyncReadStream`]. In order to use the [`HostInputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. pub fn new(reader: T) -> Self { AsyncReadStream { state: StreamState::Open, @@ -259,6 +275,8 @@ pub struct AsyncWriteStream { } impl AsyncWriteStream { + /// Create a [`AsyncWriteStream`]. In order to use the [`HostOutputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncWrite`]. pub fn new(writer: T) -> Self { AsyncWriteStream { buffer: Vec::new(), @@ -341,6 +359,16 @@ mod async_fd_stream { } impl AsyncFdStream { + /// Create a [`AsyncFdStream`] from a type which implements [`AsRawFd`]. + /// This constructor will use `fcntl(2)` to set the `O_NONBLOCK` flag + /// if it is not already set. + /// The implementation of this constructor creates an + /// [`tokio::io::unix::AsyncFd`]. It will return an error unless + /// called from inside a tokio context. Additionally, tokio (via mio) + /// will register the fd inside with `epoll(7)` (or equiv on + /// macos). The process may only make one registration of an fd at a + /// time. If another registration exists, this constructor will return + /// an error. pub fn new(fd: T) -> anyhow::Result { use rustix::fs::OFlags; let borrowed_fd = unsafe { rustix::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) }; From 6b47b1b4946b5b6fc998538b12d07ec9fbf455b7 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 15:51:41 -0700 Subject: [PATCH 056/118] Add a workspace dependency on bytes-1.4 --- Cargo.lock | 5 +++-- Cargo.toml | 1 + crates/wasi-http/Cargo.toml | 2 +- crates/wasi/Cargo.toml | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67eb572f4ae1..d51cecae9a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,9 +246,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "camino" @@ -4230,6 +4230,7 @@ dependencies = [ "anyhow", "async-trait", "bitflags 1.3.2", + "bytes", "cap-fs-ext", "cap-rand", "cap-std", diff --git a/Cargo.toml b/Cargo.toml index 4a8af6227172..96b0ffd3cdb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,6 +240,7 @@ filecheck = "0.5.0" libc = "0.2.60" file-per-thread-logger = "0.2.0" tokio = { version = "1.26.0" } +bytes = "1.4" futures-util = { version = "0.3.27", default-features = false } [features] diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index f91f9b5233e4..1f7a5d7d8ca4 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -9,7 +9,7 @@ description = "Experimental HTTP library for WebAssembly in Wasmtime" [dependencies] anyhow = { workspace = true } -bytes = "1.1.0" +bytes = { workspace = true } hyper = { version = "1.0.0-rc.3", features = ["full"] } tokio = { version = "1", default-features = false, features = ["net", "rt-multi-thread", "time"] } http = { version = "0.2.9" } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 5ee6b3e52411..0c28f707740a 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -23,6 +23,7 @@ libc = { workspace = true } once_cell = { workspace = true } tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] } +bytes = { workspace = true } thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } From 26be285e25eacfc86d82f560a9b358fc5bb1d8fa Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 15:51:52 -0700 Subject: [PATCH 057/118] Remove vectored stream operations --- crates/wasi/src/preview2/filesystem.rs | 44 -------------------------- crates/wasi/src/preview2/stream.rs | 35 -------------------- 2 files changed, 79 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 96a2813b3258..7fea38508076 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -104,21 +104,6 @@ impl HostInputStream for FileInputStream { self.position = self.position.wrapping_add(n); Ok((n, end)) } - fn read_vectored<'a>( - &mut self, - bufs: &mut [std::io::IoSliceMut<'a>], - ) -> anyhow::Result<(u64, StreamState)> { - use system_interface::fs::FileIoExt; - let (n, end) = read_result(block_in_place(|| { - self.file.read_vectored_at(bufs, self.position) - }))?; - self.position = self.position.wrapping_add(n); - Ok((n, end)) - } - fn is_read_vectored(&self) -> bool { - use system_interface::fs::FileIoExt; - self.file.is_read_vectored_at() - } async fn ready(&mut self) -> anyhow::Result<()> { Ok(()) // Always immediately ready - file reads cannot block } @@ -155,21 +140,6 @@ impl HostOutputStream for FileOutputStream { Ok(n) } - /// Vectored-I/O form of `write`. - fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { - use system_interface::fs::FileIoExt; - let n = block_in_place(|| self.file.write_vectored_at(bufs, self.position))? as i64 as u64; - self.position = self.position.wrapping_add(n); - Ok(n) - } - - /// Test whether vectored I/O writes are known to be optimized in the - /// underlying implementation. - fn is_write_vectored(&self) -> bool { - use system_interface::fs::FileIoExt; - self.file.is_write_vectored_at() - } - async fn ready(&mut self) -> anyhow::Result<()> { Ok(()) // Always immediately ready - file writes cannot block } @@ -192,20 +162,6 @@ impl HostOutputStream for FileAppendStream { Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) } - /// Vectored-I/O form of `write`. - fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> anyhow::Result { - use system_interface::fs::FileIoExt; - let n = block_in_place(|| self.file.append_vectored(bufs))? as i64 as u64; - Ok(n) - } - - /// Test whether vectored I/O writes are known to be optimized in the - /// underlying implementation. - fn is_write_vectored(&self) -> bool { - use system_interface::fs::FileIoExt; - self.file.is_write_vectored_at() - } - async fn ready(&mut self) -> anyhow::Result<()> { Ok(()) // Always immediately ready - file appends cannot block } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 6a370615a905..9db84d31afcd 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -24,25 +24,6 @@ pub trait HostInputStream: Send + Sync { /// Important: this read must be non-blocking! fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; - /// Vectored-I/O form of `read`. Important: this read must be - /// non-blocking! - fn read_vectored<'a>( - &mut self, - bufs: &mut [std::io::IoSliceMut<'a>], - ) -> Result<(u64, StreamState), Error> { - if bufs.len() > 0 { - self.read(bufs.get_mut(0).unwrap()) - } else { - self.read(&mut []) - } - } - - /// Test whether vectored I/O reads are known to be optimized in the - /// underlying implementation. - fn is_read_vectored(&self) -> bool { - false - } - /// Read bytes from a stream and discard them. Important: this method must /// be non-blocking! fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { @@ -75,22 +56,6 @@ pub trait HostOutputStream: Send + Sync { /// Important: this write must be non-blocking! fn write(&mut self, _buf: &[u8]) -> Result; - /// Vectored-I/O form of `write`. Important: this write must be - /// non-blocking! - fn write_vectored<'a>(&mut self, bufs: &[std::io::IoSlice<'a>]) -> Result { - if bufs.len() > 0 { - self.write(bufs.get(0).unwrap()) - } else { - Ok(0) - } - } - - /// Test whether vectored I/O writes are known to be optimized in the - /// underlying implementation. - fn is_write_vectored(&self) -> bool { - false - } - /// Transfer bytes directly from an input stream to an output stream. /// Important: this splice must be non-blocking! fn splice( From 173d545b0ddd155cad38426e2fa7a48ec8640d6d Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 16:13:15 -0700 Subject: [PATCH 058/118] Rework the read/write stream traits --- crates/wasi/src/preview2/filesystem.rs | 30 +-- crates/wasi/src/preview2/pipe.rs | 137 +++++++------- crates/wasi/src/preview2/preview2/io.rs | 38 ++-- crates/wasi/src/preview2/stdio.rs | 11 +- crates/wasi/src/preview2/stdio/unix.rs | 18 +- crates/wasi/src/preview2/stdio/windows.rs | 26 +-- crates/wasi/src/preview2/stream.rs | 218 +++++++++++----------- 7 files changed, 246 insertions(+), 232 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 7fea38508076..7ae419709adb 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -2,6 +2,7 @@ use crate::preview2::{ block_in_place, HostInputStream, HostOutputStream, StreamState, Table, TableError, }; use std::sync::Arc; +use bytes::Bytes; bitflags::bitflags! { pub struct FilePerms: usize { @@ -98,11 +99,12 @@ impl FileInputStream { #[async_trait::async_trait] impl HostInputStream for FileInputStream { - fn read(&mut self, buf: &mut [u8]) -> anyhow::Result<(u64, StreamState)> { - use system_interface::fs::FileIoExt; - let (n, end) = read_result(block_in_place(|| self.file.read_at(buf, self.position)))?; - self.position = self.position.wrapping_add(n); - Ok((n, end)) + fn read(&mut self) -> anyhow::Result<(Bytes, StreamState)> { + // use system_interface::fs::FileIoExt; + // let (n, end) = read_result(block_in_place(|| self.file.read_at(buf, self.position)))?; + // self.position = self.position.wrapping_add(n); + // Ok((n, end)) + todo!() } async fn ready(&mut self) -> anyhow::Result<()> { Ok(()) // Always immediately ready - file reads cannot block @@ -133,11 +135,12 @@ impl FileOutputStream { #[async_trait::async_trait] impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: &[u8]) -> anyhow::Result { - use system_interface::fs::FileIoExt; - let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; - self.position = self.position.wrapping_add(n); - Ok(n) + fn write(&mut self, buf: Bytes) -> anyhow::Result { + // use system_interface::fs::FileIoExt; + // let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; + // self.position = self.position.wrapping_add(n); + // Ok(n) + todo!() } async fn ready(&mut self) -> anyhow::Result<()> { @@ -157,9 +160,10 @@ impl FileAppendStream { #[async_trait::async_trait] impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: &[u8]) -> anyhow::Result { - use system_interface::fs::FileIoExt; - Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) + fn write(&mut self, buf: Bytes) -> anyhow::Result { + // use system_interface::fs::FileIoExt; + // Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) + todo!() } async fn ready(&mut self) -> anyhow::Result<()> { diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 6c4b00af4640..c983e751cc44 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -9,6 +9,7 @@ //! use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; use anyhow::Error; +use bytes::Bytes; pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { let (writer, reader) = tokio::sync::mpsc::channel(bound); @@ -34,35 +35,36 @@ impl InputPipe { #[async_trait::async_trait] impl HostInputStream for InputPipe { - fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - use tokio::sync::mpsc::error::TryRecvError; - let read_from_buffer = self.buffer.len().min(dest.len()); - let buffer_dest = &mut dest[..read_from_buffer]; - buffer_dest.copy_from_slice(&self.buffer[..read_from_buffer]); - // Keep remaining contents in buffer - self.buffer = self.buffer.split_off(read_from_buffer); - if read_from_buffer < dest.len() { - match self.channel.try_recv() { - Ok(msg) => { - let recv_dest = &mut dest[read_from_buffer..]; - if msg.len() < recv_dest.len() { - recv_dest[..msg.len()].copy_from_slice(&msg); - Ok(((read_from_buffer + msg.len()) as u64, self.state)) - } else { - recv_dest.copy_from_slice(&msg[..recv_dest.len()]); - self.buffer.extend_from_slice(&msg[recv_dest.len()..]); - Ok((dest.len() as u64, self.state)) - } - } - Err(TryRecvError::Empty) => Ok((read_from_buffer as u64, self.state)), - Err(TryRecvError::Disconnected) => { - self.state = StreamState::Closed; - Ok((read_from_buffer as u64, self.state)) - } - } - } else { - Ok((read_from_buffer as u64, self.state)) - } + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // use tokio::sync::mpsc::error::TryRecvError; + // let read_from_buffer = self.buffer.len().min(dest.len()); + // let buffer_dest = &mut dest[..read_from_buffer]; + // buffer_dest.copy_from_slice(&self.buffer[..read_from_buffer]); + // // Keep remaining contents in buffer + // self.buffer = self.buffer.split_off(read_from_buffer); + // if read_from_buffer < dest.len() { + // match self.channel.try_recv() { + // Ok(msg) => { + // let recv_dest = &mut dest[read_from_buffer..]; + // if msg.len() < recv_dest.len() { + // recv_dest[..msg.len()].copy_from_slice(&msg); + // Ok(((read_from_buffer + msg.len()) as u64, self.state)) + // } else { + // recv_dest.copy_from_slice(&msg[..recv_dest.len()]); + // self.buffer.extend_from_slice(&msg[recv_dest.len()..]); + // Ok((dest.len() as u64, self.state)) + // } + // } + // Err(TryRecvError::Empty) => Ok((read_from_buffer as u64, self.state)), + // Err(TryRecvError::Disconnected) => { + // self.state = StreamState::Closed; + // Ok((read_from_buffer as u64, self.state)) + // } + // } + // } else { + // Ok((read_from_buffer as u64, self.state)) + // } + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -129,32 +131,33 @@ impl OutputPipe { #[async_trait::async_trait] impl HostOutputStream for OutputPipe { - fn write(&mut self, buf: &[u8]) -> Result { - use tokio::sync::mpsc::error::TrySendError; - - let mut bytes = core::mem::take(&mut self.buffer); - bytes.extend(buf); - let (s, bytes) = match self.take_channel() { - SenderState::Writable(p) => { - let s = p.send(bytes); - (s, Vec::new()) - } - - SenderState::Channel(s) => match s.try_send(bytes) { - Ok(()) => (s, Vec::new()), - Err(TrySendError::Full(b)) => (s, b), - Err(TrySendError::Closed(_)) => { - // TODO: we may need to communicate failure out in a way that doesn't result in - // a trap. - return Err(anyhow::anyhow!("pipe closed")); - } - }, - }; - - self.buffer = bytes; - self.channel = Some(SenderState::Channel(s)); - - Ok(buf.len() as u64) + fn write(&mut self, buf: Bytes) -> Result { + // use tokio::sync::mpsc::error::TrySendError; + // + // let mut bytes = core::mem::take(&mut self.buffer); + // bytes.extend(buf); + // let (s, bytes) = match self.take_channel() { + // SenderState::Writable(p) => { + // let s = p.send(bytes); + // (s, Vec::new()) + // } + // + // SenderState::Channel(s) => match s.try_send(bytes) { + // Ok(()) => (s, Vec::new()), + // Err(TrySendError::Full(b)) => (s, b), + // Err(TrySendError::Closed(_)) => { + // // TODO: we may need to communicate failure out in a way that doesn't result in + // // a trap. + // return Err(anyhow::anyhow!("pipe closed")); + // } + // }, + // }; + // + // self.buffer = bytes; + // self.channel = Some(SenderState::Channel(s)); + // + // Ok(buf.len() as u64) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -185,14 +188,15 @@ impl MemoryInputPipe { #[async_trait::async_trait] impl HostInputStream for MemoryInputPipe { - fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - let nbytes = std::io::Read::read(&mut self.buffer, dest)?; - let state = if self.buffer.get_ref().len() as u64 == self.buffer.position() { - StreamState::Closed - } else { - StreamState::Open - }; - Ok((nbytes as u64, state)) + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // let nbytes = std::io::Read::read(&mut self.buffer, dest)?; + // let state = if self.buffer.get_ref().len() as u64 == self.buffer.position() { + // StreamState::Closed + // } else { + // StreamState::Open + // }; + // Ok((nbytes as u64, state)) + todo!() } async fn ready(&mut self) -> Result<(), Error> { if self.buffer.get_ref().len() as u64 > self.buffer.position() { @@ -227,9 +231,10 @@ impl MemoryOutputPipe { #[async_trait::async_trait] impl HostOutputStream for MemoryOutputPipe { - fn write(&mut self, buf: &[u8]) -> Result { - self.buffer.lock().unwrap().extend(buf); - Ok(buf.len() as u64) + fn write(&mut self, buf: Bytes) -> Result { + // self.buffer.lock().unwrap().extend(buf); + // Ok(buf.len() as u64) + todo!() } async fn ready(&mut self) -> Result<(), Error> { diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 31913cd3c40d..29945d2d24ee 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -48,19 +48,20 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, bool), streams::Error> { - let s = self.table_mut().get_input_stream_mut(stream)?; - - // Len could be any `u64` value, but we don't want to - // allocate too much up front, so make a wild guess - // of an upper bound for the buffer size. - let buffer_len = std::cmp::min(len, 0x400000) as _; - let mut buffer = vec![0; buffer_len]; - - let (bytes_read, state) = HostInputStream::read(s.as_mut(), &mut buffer)?; - - buffer.truncate(bytes_read as usize); - - Ok((buffer, state.is_closed())) + // let s = self.table_mut().get_input_stream_mut(stream)?; + // + // // Len could be any `u64` value, but we don't want to + // // allocate too much up front, so make a wild guess + // // of an upper bound for the buffer size. + // let buffer_len = std::cmp::min(len, 0x400000) as _; + // let mut buffer = vec![0; buffer_len]; + // + // let (bytes_read, state) = HostInputStream::read(s.as_mut(), &mut buffer)?; + // + // buffer.truncate(bytes_read as usize); + // + // Ok((buffer, state.is_closed())) + todo!() } async fn blocking_read( @@ -76,11 +77,12 @@ impl streams::Host for T { } async fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { - let s = self.table_mut().get_output_stream_mut(stream)?; - - let bytes_written: u64 = HostOutputStream::write(s.as_mut(), &bytes)?; - - Ok(u64::try_from(bytes_written).unwrap()) + // let s = self.table_mut().get_output_stream_mut(stream)?; + // + // let bytes_written: u64 = HostOutputStream::write(s.as_mut(), &bytes)?; + // + // Ok(u64::try_from(bytes_written).unwrap()) + todo!() } async fn blocking_write( diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 59de42108026..0f6c975dc480 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,4 +1,5 @@ use anyhow::Error; +use bytes::Bytes; use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; @@ -27,8 +28,9 @@ pub struct EmptyStream; #[async_trait::async_trait] impl HostInputStream for EmptyStream { - fn read(&mut self, _buf: &mut [u8]) -> Result<(u64, StreamState), Error> { - Ok((0, StreamState::Open)) + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // Ok((0, StreamState::Open)) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -38,8 +40,9 @@ impl HostInputStream for EmptyStream { #[async_trait::async_trait] impl HostOutputStream for EmptyStream { - fn write(&mut self, buf: &[u8]) -> Result { - Ok(buf.len() as u64) + fn write(&mut self, buf: Bytes) -> Result { + // Ok(buf.len() as u64) + todo!() } async fn ready(&mut self) -> Result<(), Error> { diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index bd87c2f7e5fa..14acaed2c109 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -1,5 +1,6 @@ use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; use anyhow::Error; +use bytes::Bytes; // wasmtime cant use std::sync::OnceLock yet because of a llvm regression in // 1.70. when 1.71 is released, we can switch to using std here. @@ -42,14 +43,15 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl HostInputStream for Stdin { - fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { - let mut r = move || Self::get_global().blocking_lock().read(buf); - // If we are currently in a tokio context, blocking_lock will panic unless inside a - // block_in_place: - match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::block_in_place(r), - Err(_) => r(), - } + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // let mut r = move || Self::get_global().blocking_lock().read(buf); + // // If we are currently in a tokio context, blocking_lock will panic unless inside a + // // block_in_place: + // match tokio::runtime::Handle::try_current() { + // Ok(_) => tokio::task::block_in_place(r), + // Err(_) => r(), + // } + todo!() } async fn ready(&mut self) -> Result<(), Error> { diff --git a/crates/wasi/src/preview2/stdio/windows.rs b/crates/wasi/src/preview2/stdio/windows.rs index e24cdf4b3e4e..3cc782f703a2 100644 --- a/crates/wasi/src/preview2/stdio/windows.rs +++ b/crates/wasi/src/preview2/stdio/windows.rs @@ -1,5 +1,6 @@ use crate::preview2::{HostInputStream, StreamState}; use anyhow::{Context, Error}; +use bytes::Bytes; // wasmtime cant use std::sync::OnceLock yet because of a llvm regression in // 1.70. when 1.71 is released, we can switch to using std here. @@ -60,18 +61,19 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl HostInputStream for Stdin { - fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error> { - use std::io::Read; - let mut r = move || { - let nbytes = std::io::stdin().read(buf)?; - // FIXME handle eof - Ok((nbytes as u64, StreamState::Open)) - }; - // If we are currently in a tokio context, block: - match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::block_in_place(r), - Err(_) => r(), - } + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // use std::io::Read; + // let mut r = move || { + // let nbytes = std::io::stdin().read(buf)?; + // // FIXME handle eof + // Ok((nbytes as u64, StreamState::Open)) + // }; + // // If we are currently in a tokio context, block: + // match tokio::runtime::Handle::try_current() { + // Ok(_) => tokio::task::block_in_place(r), + // Err(_) => r(), + // } + todo!() } async fn ready(&mut self) -> Result<(), Error> { diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 9db84d31afcd..bdd90a02c99d 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,5 +1,6 @@ use crate::preview2::{Table, TableError}; use anyhow::Error; +use bytes::Bytes; use std::pin::Pin; use std::task::{Context, Poll, Waker}; @@ -22,7 +23,7 @@ pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes /// read and a flag indicating whether the end of the stream was reached. /// Important: this read must be non-blocking! - fn read(&mut self, buf: &mut [u8]) -> Result<(u64, StreamState), Error>; + fn read(&mut self) -> Result<(Bytes, StreamState), Error>; /// Read bytes from a stream and discard them. Important: this method must /// be non-blocking! @@ -32,8 +33,8 @@ pub trait HostInputStream: Send + Sync { // TODO: Optimize by reading more than one byte at a time. for _ in 0..nelem { - let (num, read_state) = self.read(&mut [0])?; - nread += num; + let (bs, read_state) = self.read()?; + nread += bs.len() as u64; if read_state.is_closed() { state = read_state; break; @@ -54,7 +55,7 @@ pub trait HostInputStream: Send + Sync { pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. /// Important: this write must be non-blocking! - fn write(&mut self, _buf: &[u8]) -> Result; + fn write(&mut self, bytes: Bytes) -> Result; /// Transfer bytes directly from an input stream to an output stream. /// Important: this splice must be non-blocking! @@ -66,12 +67,10 @@ pub trait HostOutputStream: Send + Sync { let mut nspliced = 0; let mut state = StreamState::Open; - // TODO: Optimize by splicing more than one byte at a time. for _ in 0..nelem { - let mut buf = [0u8]; - let (num, read_state) = src.read(&mut buf)?; - self.write(&buf)?; - nspliced += num; + let (bs, read_state) = src.read()?; + // TODO: handle the case where write returns less than bs.len() + nspliced += self.write(bs)?; if read_state.is_closed() { state = read_state; break; @@ -84,17 +83,10 @@ pub trait HostOutputStream: Send + Sync { /// Repeatedly write a byte to a stream. Important: this write must be /// non-blocking! fn write_zeroes(&mut self, nelem: u64) -> Result { - let mut nwritten = 0; - - // TODO: Optimize by writing more than one byte at a time. - for _ in 0..nelem { - let num = self.write(&[0])?; - if num == 0 { - break; - } - nwritten += num; - } - + // TODO: We could optimize this to not allocate one big zeroed buffer, and instead write + // repeatedly from a 'static buffer of zeros. + let bs = Bytes::from_iter(core::iter::repeat(0 as u8).take(nelem as usize)); + let nwritten = self.write(bs)?; Ok(nwritten) } @@ -170,42 +162,43 @@ impl AsyncReadStream { impl HostInputStream for AsyncReadStream { - fn read(&mut self, mut dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - use std::io::Write; - let l = dest.write(&self.buffer)?; - - self.buffer.drain(..l); - if !self.buffer.is_empty() { - return Ok((l as u64, StreamState::Open)); - } - - if self.state.is_closed() { - return Ok((l as u64, StreamState::Closed)); - } - - let dest = &mut dest[l..]; - let rest = if !dest.is_empty() { - let mut readbuf = tokio::io::ReadBuf::new(dest); - - let noop_waker = noop_waker(); - let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // Make a synchronous, non-blocking call attempt to read. We are not - // going to poll this more than once, so the noop waker is appropriate. - match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { - Poll::Pending => {} // Nothing was read - Poll::Ready(result) => result?, // Maybe an error occured - }; - let bytes_read = readbuf.filled().len(); - - if bytes_read == 0 { - self.state = StreamState::Closed; - } - bytes_read - } else { - 0 - }; - - Ok(((l + rest) as u64, self.state)) + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // use std::io::Write; + // let l = dest.write(&self.buffer)?; + // + // self.buffer.drain(..l); + // if !self.buffer.is_empty() { + // return Ok((l as u64, StreamState::Open)); + // } + // + // if self.state.is_closed() { + // return Ok((l as u64, StreamState::Closed)); + // } + // + // let dest = &mut dest[l..]; + // let rest = if !dest.is_empty() { + // let mut readbuf = tokio::io::ReadBuf::new(dest); + // + // let noop_waker = noop_waker(); + // let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // // Make a synchronous, non-blocking call attempt to read. We are not + // // going to poll this more than once, so the noop waker is appropriate. + // match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { + // Poll::Pending => {} // Nothing was read + // Poll::Ready(result) => result?, // Maybe an error occured + // }; + // let bytes_read = readbuf.filled().len(); + // + // if bytes_read == 0 { + // self.state = StreamState::Closed; + // } + // bytes_read + // } else { + // 0 + // }; + // + // Ok(((l + rest) as u64, self.state)) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -254,26 +247,26 @@ impl AsyncWriteStream { impl HostOutputStream for AsyncWriteStream { - // I can get rid of the `async` here once the lock is no longer a tokio lock: - fn write(&mut self, buf: &[u8]) -> Result { - let mut bytes = core::mem::take(&mut self.buffer); - bytes.extend(buf); - - let noop_waker = noop_waker(); - let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // Make a synchronous, non-blocking call attempt to write. We are not - // going to poll this more than once, so the noop waker is appropriate. - match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { - Poll::Pending => { - // Nothing was written: buffer all of it below. - } - Poll::Ready(written) => { - // So much was written: - bytes.drain(..written?); - } - } - self.buffer = bytes; - Ok(buf.len() as u64) + fn write(&mut self, bytes: Bytes) -> Result { + // let mut bytes = core::mem::take(&mut self.buffer); + // bytes.extend(buf); + // + // let noop_waker = noop_waker(); + // let mut cx: Context<'_> = Context::from_waker(&noop_waker); + // // Make a synchronous, non-blocking call attempt to write. We are not + // // going to poll this more than once, so the noop waker is appropriate. + // match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { + // Poll::Pending => { + // // Nothing was written: buffer all of it below. + // } + // Poll::Ready(written) => { + // // So much was written: + // bytes.drain(..written?); + // } + // } + // self.buffer = bytes; + // Ok(buf.len() as u64) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -313,6 +306,7 @@ pub use async_fd_stream::*; mod async_fd_stream { use super::{HostInputStream, HostOutputStream, StreamState}; use anyhow::Error; + use bytes::Bytes; use std::io::{Read, Write}; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use tokio::io::unix::AsyncFd; @@ -352,23 +346,24 @@ mod async_fd_stream { #[async_trait::async_trait] impl HostInputStream for AsyncFdStream { - fn read(&mut self, dest: &mut [u8]) -> Result<(u64, StreamState), Error> { - // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // we're done. - let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - - // Ensured this is nonblocking at construction of AsyncFdStream. - let read_res = file.read(dest); - - // Make sure that the file doesn't close the fd when it's dropped. - file.into_raw_fd(); - - let n = read_res?; - - // TODO: figure out when the stream should be considered closed - // TODO: figure out how to handle the error conditions from the read call above - - Ok((n as u64, StreamState::Open)) + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // // we're done. + // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + // + // // Ensured this is nonblocking at construction of AsyncFdStream. + // let read_res = file.read(dest); + // + // // Make sure that the file doesn't close the fd when it's dropped. + // file.into_raw_fd(); + // + // let n = read_res?; + // + // // TODO: figure out when the stream should be considered closed + // // TODO: figure out how to handle the error conditions from the read call above + // + // Ok((n as u64, StreamState::Open)) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -379,22 +374,23 @@ mod async_fd_stream { #[async_trait::async_trait] impl HostOutputStream for AsyncFdStream { - fn write(&mut self, buf: &[u8]) -> Result { - // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // we're done. - let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - - // Ensured this is nonblocking at construction of AsyncFdStream. - let write_res = file.write(buf); - - // Make sure that the file doesn't close the fd when it's dropped. - file.into_raw_fd(); - - let n = write_res?; - - // TODO: figure out how to handle the error conditions from the write call above - - Ok(n as u64) + fn write(&mut self, bytes: Bytes) -> Result { + // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when + // // we're done. + // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; + // + // // Ensured this is nonblocking at construction of AsyncFdStream. + // let write_res = file.write(buf); + // + // // Make sure that the file doesn't close the fd when it's dropped. + // file.into_raw_fd(); + // + // let n = write_res?; + // + // // TODO: figure out how to handle the error conditions from the write call above + // + // Ok(n as u64) + todo!() } async fn ready(&mut self) -> Result<(), Error> { @@ -412,7 +408,7 @@ mod test { struct DummyInputStream; #[async_trait::async_trait] impl HostInputStream for DummyInputStream { - fn read(&mut self, _: &mut [u8]) -> Result<(u64, StreamState), Error> { + fn read(&mut self) -> Result<(Bytes, StreamState), Error> { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { @@ -433,7 +429,7 @@ mod test { struct DummyOutputStream; #[async_trait::async_trait] impl HostOutputStream for DummyOutputStream { - fn write(&mut self, _: &[u8]) -> Result { + fn write(&mut self, _: Bytes) -> Result { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { From 9ae47548b5027ba38837b0b8c96e19276120df10 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 16:23:43 -0700 Subject: [PATCH 059/118] Add a size parameter to `read`, and switch to usize for traits --- crates/wasi/src/preview2/filesystem.rs | 6 +-- crates/wasi/src/preview2/pipe.rs | 8 ++-- crates/wasi/src/preview2/preview2/io.rs | 10 +++-- crates/wasi/src/preview2/stdio.rs | 4 +- crates/wasi/src/preview2/stdio/unix.rs | 2 +- crates/wasi/src/preview2/stdio/windows.rs | 2 +- crates/wasi/src/preview2/stream.rs | 53 ++++++++++------------- 7 files changed, 41 insertions(+), 44 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 7ae419709adb..fd5140c65bd1 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -99,7 +99,7 @@ impl FileInputStream { #[async_trait::async_trait] impl HostInputStream for FileInputStream { - fn read(&mut self) -> anyhow::Result<(Bytes, StreamState)> { + fn read(&mut self, size: usize) -> anyhow::Result<(Bytes, StreamState)> { // use system_interface::fs::FileIoExt; // let (n, end) = read_result(block_in_place(|| self.file.read_at(buf, self.position)))?; // self.position = self.position.wrapping_add(n); @@ -135,7 +135,7 @@ impl FileOutputStream { #[async_trait::async_trait] impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result { + fn write(&mut self, buf: Bytes) -> anyhow::Result { // use system_interface::fs::FileIoExt; // let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; // self.position = self.position.wrapping_add(n); @@ -160,7 +160,7 @@ impl FileAppendStream { #[async_trait::async_trait] impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result { + fn write(&mut self, buf: Bytes) -> anyhow::Result { // use system_interface::fs::FileIoExt; // Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) todo!() diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index c983e751cc44..dd4dd9441fdf 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -35,7 +35,7 @@ impl InputPipe { #[async_trait::async_trait] impl HostInputStream for InputPipe { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // use tokio::sync::mpsc::error::TryRecvError; // let read_from_buffer = self.buffer.len().min(dest.len()); // let buffer_dest = &mut dest[..read_from_buffer]; @@ -131,7 +131,7 @@ impl OutputPipe { #[async_trait::async_trait] impl HostOutputStream for OutputPipe { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result { // use tokio::sync::mpsc::error::TrySendError; // // let mut bytes = core::mem::take(&mut self.buffer); @@ -188,7 +188,7 @@ impl MemoryInputPipe { #[async_trait::async_trait] impl HostInputStream for MemoryInputPipe { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // let nbytes = std::io::Read::read(&mut self.buffer, dest)?; // let state = if self.buffer.get_ref().len() as u64 == self.buffer.position() { // StreamState::Closed @@ -231,7 +231,7 @@ impl MemoryOutputPipe { #[async_trait::async_trait] impl HostOutputStream for MemoryOutputPipe { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result { // self.buffer.lock().unwrap().extend(buf); // Ok(buf.len() as u64) todo!() diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 29945d2d24ee..f466883e6f01 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -101,9 +101,10 @@ impl streams::Host for T { async fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { let s = self.table_mut().get_input_stream_mut(stream)?; - let (bytes_skipped, end) = s.skip(len)?; + // TODO: the cast to usize should be fallible, use `.try_into()?` + let (bytes_skipped, end) = s.skip(len as usize)?; - Ok((bytes_skipped, end.is_closed())) + Ok((bytes_skipped as u64, end.is_closed())) } async fn blocking_skip( @@ -126,9 +127,10 @@ impl streams::Host for T { ) -> Result { let s = self.table_mut().get_output_stream_mut(stream)?; - let bytes_written: u64 = s.write_zeroes(len)?; + // TODO: the cast to usize should be fallible, use `.try_into()?` + let bytes_written = s.write_zeroes(len as usize)?; - Ok(bytes_written) + Ok(bytes_written as u64) } async fn blocking_write_zeroes( diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 0f6c975dc480..86e34e10cd95 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -28,7 +28,7 @@ pub struct EmptyStream; #[async_trait::async_trait] impl HostInputStream for EmptyStream { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // Ok((0, StreamState::Open)) todo!() } @@ -40,7 +40,7 @@ impl HostInputStream for EmptyStream { #[async_trait::async_trait] impl HostOutputStream for EmptyStream { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result { // Ok(buf.len() as u64) todo!() } diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index 14acaed2c109..2ade52dd726d 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -43,7 +43,7 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl HostInputStream for Stdin { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // let mut r = move || Self::get_global().blocking_lock().read(buf); // // If we are currently in a tokio context, blocking_lock will panic unless inside a // // block_in_place: diff --git a/crates/wasi/src/preview2/stdio/windows.rs b/crates/wasi/src/preview2/stdio/windows.rs index 3cc782f703a2..fc39afdf7682 100644 --- a/crates/wasi/src/preview2/stdio/windows.rs +++ b/crates/wasi/src/preview2/stdio/windows.rs @@ -61,7 +61,7 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl HostInputStream for Stdin { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // use std::io::Read; // let mut r = move || { // let nbytes = std::io::stdin().read(buf)?; diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index bdd90a02c99d..5a39eb968b32 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -23,22 +23,19 @@ pub trait HostInputStream: Send + Sync { /// Read bytes. On success, returns a pair holding the number of bytes /// read and a flag indicating whether the end of the stream was reached. /// Important: this read must be non-blocking! - fn read(&mut self) -> Result<(Bytes, StreamState), Error>; + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error>; /// Read bytes from a stream and discard them. Important: this method must /// be non-blocking! - fn skip(&mut self, nelem: u64) -> Result<(u64, StreamState), Error> { + fn skip(&mut self, nelem: usize) -> Result<(usize, StreamState), Error> { let mut nread = 0; let mut state = StreamState::Open; - // TODO: Optimize by reading more than one byte at a time. - for _ in 0..nelem { - let (bs, read_state) = self.read()?; - nread += bs.len() as u64; - if read_state.is_closed() { - state = read_state; - break; - } + let (bs, read_state) = self.read(nelem)?; + // TODO: handle the case where `bs.len()` is less than `nelem` + nread += bs.len(); + if read_state.is_closed() { + state = read_state; } Ok((nread, state)) @@ -55,26 +52,24 @@ pub trait HostInputStream: Send + Sync { pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. /// Important: this write must be non-blocking! - fn write(&mut self, bytes: Bytes) -> Result; + fn write(&mut self, bytes: Bytes) -> Result; /// Transfer bytes directly from an input stream to an output stream. /// Important: this splice must be non-blocking! fn splice( &mut self, src: &mut dyn HostInputStream, - nelem: u64, - ) -> Result<(u64, StreamState), Error> { + nelem: usize, + ) -> Result<(usize, StreamState), Error> { let mut nspliced = 0; let mut state = StreamState::Open; - for _ in 0..nelem { - let (bs, read_state) = src.read()?; - // TODO: handle the case where write returns less than bs.len() - nspliced += self.write(bs)?; - if read_state.is_closed() { - state = read_state; - break; - } + let (bs, read_state) = src.read(nelem)?; + // TODO: handle the case where write returns less than `bs.len()` + // TODO: handle the case where `bs.len()` is less than `nelem` + nspliced += self.write(bs)?; + if read_state.is_closed() { + state = read_state; } Ok((nspliced, state)) @@ -82,10 +77,10 @@ pub trait HostOutputStream: Send + Sync { /// Repeatedly write a byte to a stream. Important: this write must be /// non-blocking! - fn write_zeroes(&mut self, nelem: u64) -> Result { + fn write_zeroes(&mut self, nelem: usize) -> Result { // TODO: We could optimize this to not allocate one big zeroed buffer, and instead write // repeatedly from a 'static buffer of zeros. - let bs = Bytes::from_iter(core::iter::repeat(0 as u8).take(nelem as usize)); + let bs = Bytes::from_iter(core::iter::repeat(0 as u8).take(nelem)); let nwritten = self.write(bs)?; Ok(nwritten) } @@ -162,7 +157,7 @@ impl AsyncReadStream { impl HostInputStream for AsyncReadStream { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // use std::io::Write; // let l = dest.write(&self.buffer)?; // @@ -247,7 +242,7 @@ impl AsyncWriteStream { impl HostOutputStream for AsyncWriteStream { - fn write(&mut self, bytes: Bytes) -> Result { + fn write(&mut self, bytes: Bytes) -> Result { // let mut bytes = core::mem::take(&mut self.buffer); // bytes.extend(buf); // @@ -346,7 +341,7 @@ mod async_fd_stream { #[async_trait::async_trait] impl HostInputStream for AsyncFdStream { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when // // we're done. // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; @@ -374,7 +369,7 @@ mod async_fd_stream { #[async_trait::async_trait] impl HostOutputStream for AsyncFdStream { - fn write(&mut self, bytes: Bytes) -> Result { + fn write(&mut self, bytes: Bytes) -> Result { // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when // // we're done. // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; @@ -408,7 +403,7 @@ mod test { struct DummyInputStream; #[async_trait::async_trait] impl HostInputStream for DummyInputStream { - fn read(&mut self) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { @@ -429,7 +424,7 @@ mod test { struct DummyOutputStream; #[async_trait::async_trait] impl HostOutputStream for DummyOutputStream { - fn write(&mut self, _: Bytes) -> Result { + fn write(&mut self, _: Bytes) -> Result { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { From 70b0b76799f71cc08c93b5714b1529b805126d38 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 16:53:11 -0700 Subject: [PATCH 060/118] Pipe through the bool -> stream-status change in wit --- crates/wasi/src/preview2/preview1/mod.rs | 20 ++++----- crates/wasi/src/preview2/preview2/io.rs | 54 ++++++++++++++++++------ crates/wasi/wit/deps/io/streams.wit | 38 +++++++++++++---- 3 files changed, 82 insertions(+), 30 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index 1876544bfcca..d60be6aedfbc 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1047,7 +1047,7 @@ impl< iovs: &types::IovecArray<'a>, ) -> Result { let desc = self.transact()?.get_descriptor(fd)?.clone(); - let (mut buf, read, end) = match desc { + let (mut buf, read, state) = match desc { Descriptor::File(File { fd, blocking, @@ -1065,7 +1065,7 @@ impl< .unwrap_or_else(types::Error::trap) })?; let max = buf.len().try_into().unwrap_or(u64::MAX); - let (read, end) = if blocking { + let (read, state) = if blocking { streams::Host::blocking_read(self, stream, max).await } else { streams::Host::read(self, stream, max).await @@ -1076,24 +1076,24 @@ impl< let pos = pos.checked_add(n).ok_or(types::Errno::Overflow)?; position.store(pos, Ordering::Relaxed); - (buf, read, end) + (buf, read, state) } Descriptor::Stdin(stream) => { let Some(buf) = first_non_empty_iovec(iovs)? else { return Ok(0) }; - let (read, end) = + let (read, state) = streams::Host::read(self, stream, buf.len().try_into().unwrap_or(u64::MAX)) .await .map_err(|_| types::Errno::Io)?; - (buf, read, end) + (buf, read, state) } _ => return Err(types::Errno::Badf.into()), }; if read.len() > buf.len() { return Err(types::Errno::Range.into()); } - if !end && read.len() == 0 { + if state == streams::StreamStatus::Open && read.len() == 0 { return Err(types::Errno::Intr.into()); } let (buf, _) = buf.split_at_mut(read.len()); @@ -1112,7 +1112,7 @@ impl< offset: types::Filesize, ) -> Result { let desc = self.transact()?.get_descriptor(fd)?.clone(); - let (mut buf, read, end) = match desc { + let (mut buf, read, state) = match desc { Descriptor::File(File { fd, blocking, .. }) if self.table().is_file(fd) => { let Some(buf) = first_non_empty_iovec(iovs)? else { return Ok(0) @@ -1124,14 +1124,14 @@ impl< .unwrap_or_else(types::Error::trap) })?; let max = buf.len().try_into().unwrap_or(u64::MAX); - let (read, end) = if blocking { + let (read, state) = if blocking { streams::Host::blocking_read(self, stream, max).await } else { streams::Host::read(self, stream, max).await } .map_err(|_| types::Errno::Io)?; - (buf, read, end) + (buf, read, state) } Descriptor::Stdin(..) => { // NOTE: legacy implementation returns SPIPE here @@ -1142,7 +1142,7 @@ impl< if read.len() > buf.len() { return Err(types::Errno::Range.into()); } - if !end && read.len() == 0 { + if state == streams::StreamStatus::Open && read.len() == 0 { return Err(types::Errno::Intr.into()); } let (buf, _) = buf.split_at_mut(read.len()); diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index f466883e6f01..c2d502c9e08e 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -2,7 +2,7 @@ use crate::preview2::{ bindings::io::streams::{self, InputStream, OutputStream, StreamError}, bindings::poll::poll::Pollable, poll::PollableFuture, - stream::{HostInputStream, HostOutputStream, TableStreamExt}, + stream::{HostInputStream, HostOutputStream, TableStreamExt, StreamState}, HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -29,6 +29,15 @@ impl From for streams::Error { } } +impl From for streams::StreamStatus { + fn from(state: StreamState) -> Self { + match state { + StreamState::Open => Self::Open, + StreamState::Closed => Self::Ended, + } + } +} + #[async_trait::async_trait] impl streams::Host for T { async fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { @@ -47,7 +56,7 @@ impl streams::Host for T { &mut self, stream: InputStream, len: u64, - ) -> Result<(Vec, bool), streams::Error> { + ) -> Result<(Vec, streams::StreamStatus), streams::Error> { // let s = self.table_mut().get_input_stream_mut(stream)?; // // // Len could be any `u64` value, but we don't want to @@ -68,7 +77,7 @@ impl streams::Host for T { &mut self, stream: InputStream, len: u64, - ) -> Result<(Vec, bool), streams::Error> { + ) -> Result<(Vec, streams::StreamStatus), streams::Error> { self.table_mut() .get_input_stream_mut(stream)? .ready() @@ -98,20 +107,24 @@ impl streams::Host for T { Ok(written) } - async fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { + async fn skip( + &mut self, + stream: InputStream, + len: u64, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { let s = self.table_mut().get_input_stream_mut(stream)?; // TODO: the cast to usize should be fallible, use `.try_into()?` - let (bytes_skipped, end) = s.skip(len as usize)?; + let (bytes_skipped, state) = s.skip(len as usize)?; - Ok((bytes_skipped as u64, end.is_closed())) + Ok((bytes_skipped as u64, state.into())) } async fn blocking_skip( &mut self, stream: InputStream, len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { let r = self.skip(stream, len).await?; self.table_mut() .get_input_stream_mut(stream)? @@ -263,12 +276,21 @@ impl streams::Host for T { pub mod sync { use crate::preview2::{ - bindings::io::streams::Host as AsyncHost, + bindings::io::streams::{Host as AsyncHost, StreamStatus as AsyncStreamStatus}, bindings::sync_io::io::streams::{self, InputStream, OutputStream}, bindings::sync_io::poll::poll::Pollable, block_on, WasiView, }; + impl From for streams::StreamStatus { + fn from(other: AsyncStreamStatus) -> Self { + match other { + AsyncStreamStatus::Open => Self::Open, + AsyncStreamStatus::Ended => Self::Ended, + } + } + } + impl streams::Host for T { fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { block_on(async { AsyncHost::drop_input_stream(self, stream).await }) @@ -282,8 +304,9 @@ pub mod sync { &mut self, stream: InputStream, len: u64, - ) -> Result<(Vec, bool), streams::Error> { + ) -> Result<(Vec, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::read(self, stream, len).await }) + .map(|(a,b)| (a, b.into())) .map_err(streams::Error::from) } @@ -291,8 +314,9 @@ pub mod sync { &mut self, stream: InputStream, len: u64, - ) -> Result<(Vec, bool), streams::Error> { + ) -> Result<(Vec, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_read(self, stream, len).await }) + .map(|(a,b)| (a, b.into())) .map_err(streams::Error::from) } @@ -310,8 +334,13 @@ pub mod sync { .map_err(streams::Error::from) } - fn skip(&mut self, stream: InputStream, len: u64) -> Result<(u64, bool), streams::Error> { + fn skip( + &mut self, + stream: InputStream, + len: u64, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::skip(self, stream, len).await }) + .map(|(a,b)| (a, b.into())) .map_err(streams::Error::from) } @@ -319,8 +348,9 @@ pub mod sync { &mut self, stream: InputStream, len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) + .map(|(a,b)| (a, b.into())) .map_err(streams::Error::from) } diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi/wit/deps/io/streams.wit index 008e36cf59c8..efc629ef0441 100644 --- a/crates/wasi/wit/deps/io/streams.wit +++ b/crates/wasi/wit/deps/io/streams.wit @@ -12,6 +12,22 @@ interface streams { /// doesn't provide any additional information. record stream-error {} + /// Streams provide a sequence of data and then end; once they end, they + /// no longer provide any further data. + /// + /// For example, a stream reading from a file ends when the stream reaches + /// the end of the file. For another example, a stream reading from a + /// socket ends when the socket is closed. + enum stream-status { + /// The stream is open and may produce further data. + open, + /// When reading, this indicates that the stream will not produce + /// further data. + /// When writing, this indicates that the stream will no longer be read. + /// Further writes are still permitted. + ended, + } + /// An input bytestream. In the future, this will be replaced by handle /// types. /// @@ -31,12 +47,12 @@ interface streams { /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). type input-stream = u32 - /// Read bytes from a stream. + /// Perform a non-blocking read from the stream. /// /// This function returns a list of bytes containing the data that was - /// read, along with a bool which, when true, indicates that the end of the - /// stream was reached. The returned list will contain up to `len` bytes; it - /// may return fewer than requested, but not more. + /// read, along with a stream-status which, indicates whether further reads + /// are expected to produce data. The returned list will contain up to `len` + /// bytes; it may return fewer than requested, but not more. /// /// Once a stream has reached the end, subsequent calls to read or /// `skip` will always report end-of-stream rather than producing more @@ -49,11 +65,17 @@ interface streams { /// The len here is a `u64`, but some callees may not be able to allocate /// a buffer as large as that would imply. /// FIXME: describe what happens if allocation fails. + /// + /// When the returned stream-status is open, the length of the returned + /// value may be less than `len`. When an empty list is returned, this + /// indicates that no more bytes were available from the stream at that + /// time. In that case the subscribe-to-input-stream pollable will indicate + /// when additional bytes are available for reading. read: func( this: input-stream, /// The maximum number of bytes to read len: u64 - ) -> result, bool>, stream-error> + ) -> result, stream-status>, stream-error> /// Read bytes from a stream, with blocking. /// @@ -63,7 +85,7 @@ interface streams { this: input-stream, /// The maximum number of bytes to read len: u64 - ) -> result, bool>, stream-error> + ) -> result, stream-status>, stream-error> /// Skip bytes from a stream. /// @@ -81,7 +103,7 @@ interface streams { this: input-stream, /// The maximum number of bytes to skip. len: u64, - ) -> result, stream-error> + ) -> result, stream-error> /// Skip bytes from a stream, with blocking. /// @@ -91,7 +113,7 @@ interface streams { this: input-stream, /// The maximum number of bytes to skip. len: u64, - ) -> result, stream-error> + ) -> result, stream-error> /// Create a `pollable` which will resolve once either the specified stream /// has bytes available to read or the other end of the stream has been From 99cb34180718b495e39495e362d48d8c1719c0dd Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 12 Jul 2023 17:14:39 -0700 Subject: [PATCH 061/118] Plumb stream-status through write operations in wit --- crates/wasi/src/preview2/preview1/mod.rs | 19 +++--- crates/wasi/src/preview2/preview2/io.rs | 77 +++++++++++++++--------- crates/wasi/wit/deps/io/streams.wit | 43 ++++++++----- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index d60be6aedfbc..45b4f1c3f5b0 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1186,7 +1186,7 @@ impl< })?; (stream, position) }; - let n = if blocking { + let (n, _stat) = if blocking { streams::Host::blocking_write(self, stream, buf).await } else { streams::Host::write(self, stream, buf).await @@ -1202,14 +1202,14 @@ impl< let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - streams::Host::write(self, stream, buf) + let (n, _stat) = streams::Host::write(self, stream, buf) .await - .map_err(|_| types::Errno::Io)? + .map_err(|_| types::Errno::Io)?; + n } _ => return Err(types::Errno::Badf.into()), - } - .try_into() - .or(Err(types::Errno::Overflow))?; + }; + let n = n.try_into().or(Err(types::Errno::Overflow))?; Ok(n) } @@ -1223,7 +1223,7 @@ impl< offset: types::Filesize, ) -> Result { let desc = self.transact()?.get_descriptor(fd)?.clone(); - let n = match desc { + let (n, _stat) = match desc { Descriptor::File(File { fd, blocking, .. }) if self.table().is_file(fd) => { let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) @@ -1245,9 +1245,8 @@ impl< return Err(types::Errno::Spipe.into()); } _ => return Err(types::Errno::Badf.into()), - } - .try_into() - .or(Err(types::Errno::Overflow))?; + }; + let n = n.try_into().or(Err(types::Errno::Overflow))?; Ok(n) } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index c2d502c9e08e..44b11969d996 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -2,7 +2,7 @@ use crate::preview2::{ bindings::io::streams::{self, InputStream, OutputStream, StreamError}, bindings::poll::poll::Pollable, poll::PollableFuture, - stream::{HostInputStream, HostOutputStream, TableStreamExt, StreamState}, + stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}, HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -85,7 +85,11 @@ impl streams::Host for T { self.read(stream, len).await } - async fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { + async fn write( + &mut self, + stream: OutputStream, + bytes: Vec, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { // let s = self.table_mut().get_output_stream_mut(stream)?; // // let bytes_written: u64 = HostOutputStream::write(s.as_mut(), &bytes)?; @@ -98,7 +102,7 @@ impl streams::Host for T { &mut self, stream: OutputStream, bytes: Vec, - ) -> Result { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { let written = self.write(stream, bytes).await?; self.table_mut() .get_output_stream_mut(stream)? @@ -137,20 +141,21 @@ impl streams::Host for T { &mut self, stream: OutputStream, len: u64, - ) -> Result { - let s = self.table_mut().get_output_stream_mut(stream)?; - - // TODO: the cast to usize should be fallible, use `.try_into()?` - let bytes_written = s.write_zeroes(len as usize)?; - - Ok(bytes_written as u64) + ) -> Result<(u64, streams::StreamStatus), streams::Error> { + // let s = self.table_mut().get_output_stream_mut(stream)?; + // + // // TODO: the cast to usize should be fallible, use `.try_into()?` + // let bytes_written = s.write_zeroes(len as usize)?; + // + // Ok(bytes_written as u64) + todo!() } async fn blocking_write_zeroes( &mut self, stream: OutputStream, len: u64, - ) -> Result { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { let r = self.write_zeroes(stream, len).await?; self.table_mut() .get_output_stream_mut(stream)? @@ -164,7 +169,7 @@ impl streams::Host for T { _src: InputStream, _dst: OutputStream, _len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { // TODO: We can't get two streams at the same time because they both // carry the exclusive lifetime of `ctx`. When [`get_many_mut`] is // stabilized, that could allow us to add a `get_many_stream_mut` or @@ -193,10 +198,9 @@ impl streams::Host for T { _src: InputStream, _dst: OutputStream, _len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { // TODO: once splice is implemented, figure out what the blocking semantics are for waiting - // on src and dest here. probably just delete these blocking_ funcs altogether and defer - // that decision to userland, though. + // on src and dest here. todo!("stream splice is not implemented") } @@ -204,7 +208,7 @@ impl streams::Host for T { &mut self, _src: InputStream, _dst: OutputStream, - ) -> Result { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { // TODO: We can't get two streams at the same time because they both // carry the exclusive lifetime of `ctx`. When [`get_many_mut`] is // stabilized, that could allow us to add a `get_many_stream_mut` or @@ -306,7 +310,7 @@ pub mod sync { len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::read(self, stream, len).await }) - .map(|(a,b)| (a, b.into())) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -316,12 +320,17 @@ pub mod sync { len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_read(self, stream, len).await }) - .map(|(a,b)| (a, b.into())) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } - fn write(&mut self, stream: OutputStream, bytes: Vec) -> Result { + fn write( + &mut self, + stream: OutputStream, + bytes: Vec, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::write(self, stream, bytes).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -329,8 +338,9 @@ pub mod sync { &mut self, stream: OutputStream, bytes: Vec, - ) -> Result { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::write(self, stream, bytes).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -340,7 +350,7 @@ pub mod sync { len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::skip(self, stream, len).await }) - .map(|(a,b)| (a, b.into())) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -350,12 +360,17 @@ pub mod sync { len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) - .map(|(a,b)| (a, b.into())) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } - fn write_zeroes(&mut self, stream: OutputStream, len: u64) -> Result { + fn write_zeroes( + &mut self, + stream: OutputStream, + len: u64, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::write_zeroes(self, stream, len).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -363,8 +378,9 @@ pub mod sync { &mut self, stream: OutputStream, len: u64, - ) -> Result { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -373,8 +389,9 @@ pub mod sync { src: InputStream, dst: OutputStream, len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::splice(self, src, dst, len).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -383,13 +400,19 @@ pub mod sync { src: InputStream, dst: OutputStream, len: u64, - ) -> Result<(u64, bool), streams::Error> { + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::blocking_splice(self, src, dst, len).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } - fn forward(&mut self, src: InputStream, dst: OutputStream) -> Result { + fn forward( + &mut self, + src: InputStream, + dst: OutputStream, + ) -> Result<(u64, streams::StreamStatus), streams::Error> { block_on(async { AsyncHost::forward(self, src, dst).await }) + .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi/wit/deps/io/streams.wit index efc629ef0441..0943759ab32e 100644 --- a/crates/wasi/wit/deps/io/streams.wit +++ b/crates/wasi/wit/deps/io/streams.wit @@ -50,9 +50,9 @@ interface streams { /// Perform a non-blocking read from the stream. /// /// This function returns a list of bytes containing the data that was - /// read, along with a stream-status which, indicates whether further reads - /// are expected to produce data. The returned list will contain up to `len` - /// bytes; it may return fewer than requested, but not more. + /// read, along with a `stream-status` which, indicates whether further + /// reads are expected to produce data. The returned list will contain up to + /// `len` bytes; it may return fewer than requested, but not more. /// /// Once a stream has reached the end, subsequent calls to read or /// `skip` will always report end-of-stream rather than producing more @@ -66,7 +66,7 @@ interface streams { /// a buffer as large as that would imply. /// FIXME: describe what happens if allocation fails. /// - /// When the returned stream-status is open, the length of the returned + /// When the returned `stream-status` is `open`, the length of the returned /// value may be less than `len`. When an empty list is returned, this /// indicates that no more bytes were available from the stream at that /// time. In that case the subscribe-to-input-stream pollable will indicate @@ -135,7 +135,7 @@ interface streams { /// always return promptly, after the number of bytes that can be written /// promptly, which could even be zero. To wait for the stream to be ready to /// accept data, the `subscribe-to-output-stream` function to obtain a - /// `pollable` which can be polled for using `wasi_poll`. + /// `pollable` which can be polled for using `wasi:poll`. /// /// And at present, it is a `u32` instead of being an actual handle, until /// the wit-bindgen implementation of handles and resources is ready. @@ -143,15 +143,25 @@ interface streams { /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). type output-stream = u32 - /// Write bytes to a stream. + /// Perform a non-blocking write of bytes to a stream. /// - /// This function returns a `u64` indicating the number of bytes from - /// `buf` that were written; it may be less than the full list. + /// This function returns a `u64` and a `stream-status`. The `u64` indicates + /// the number of bytes from `buf` that were written, which may be less than + /// the length of `buf`. The `stream-status` indicates if further writes to + /// the stream are expected to be read. + /// + /// When the returned `stream-status` is `open`, the `u64` return value may + /// be less than the length of `buf`. This indicates that no more bytes may + /// be written to the stream promptly. In that case the + /// subscribe-to-output-stream pollable will indicate when additional bytes + /// may be promptly written. + /// + /// TODO: document what happens when an empty list is written write: func( this: output-stream, /// Data to write buf: list - ) -> result + ) -> result, stream-error> /// Write bytes to a stream, with blocking. /// @@ -161,7 +171,7 @@ interface streams { this: output-stream, /// Data to write buf: list - ) -> result + ) -> result, stream-error> /// Write multiple zero bytes to a stream. /// @@ -171,7 +181,7 @@ interface streams { this: output-stream, /// The number of zero bytes to write len: u64 - ) -> result + ) -> result, stream-error> /// Write multiple zero bytes to a stream, with blocking. /// @@ -181,7 +191,7 @@ interface streams { this: output-stream, /// The number of zero bytes to write len: u64 - ) -> result + ) -> result, stream-error> /// Read from one stream and write to another. /// @@ -196,7 +206,7 @@ interface streams { src: input-stream, /// The number of bytes to splice len: u64, - ) -> result, stream-error> + ) -> result, stream-error> /// Read from one stream and write to another, with blocking. /// @@ -208,7 +218,7 @@ interface streams { src: input-stream, /// The number of bytes to splice len: u64, - ) -> result, stream-error> + ) -> result, stream-error> /// Forward the entire contents of an input stream to an output stream. /// @@ -220,12 +230,13 @@ interface streams { /// of the input stream is seen and all the data has been written to /// the output stream. /// - /// This function returns the number of bytes transferred. + /// This function returns the number of bytes transferred, and the status of + /// the output stream. forward: func( this: output-stream, /// The stream to read from src: input-stream - ) -> result + ) -> result, stream-error> /// Create a `pollable` which will resolve once either the specified stream /// is ready to accept bytes or the other end of the stream has been closed. From 6e122cf663e2cb73843c67c94abbf6e53de8f0d8 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 13 Jul 2023 15:31:26 -0700 Subject: [PATCH 062/118] write host trait also gives streamstate --- crates/wasi/src/preview2/filesystem.rs | 6 +++--- crates/wasi/src/preview2/pipe.rs | 4 ++-- crates/wasi/src/preview2/stdio.rs | 2 +- crates/wasi/src/preview2/stream.rs | 20 ++++++++++++-------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index fd5140c65bd1..cb97ee86b754 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,8 +1,8 @@ use crate::preview2::{ block_in_place, HostInputStream, HostOutputStream, StreamState, Table, TableError, }; -use std::sync::Arc; use bytes::Bytes; +use std::sync::Arc; bitflags::bitflags! { pub struct FilePerms: usize { @@ -135,7 +135,7 @@ impl FileOutputStream { #[async_trait::async_trait] impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result { + fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { // use system_interface::fs::FileIoExt; // let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; // self.position = self.position.wrapping_add(n); @@ -160,7 +160,7 @@ impl FileAppendStream { #[async_trait::async_trait] impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result { + fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { // use system_interface::fs::FileIoExt; // Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) todo!() diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index dd4dd9441fdf..5e43ffdefa7f 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -131,7 +131,7 @@ impl OutputPipe { #[async_trait::async_trait] impl HostOutputStream for OutputPipe { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { // use tokio::sync::mpsc::error::TrySendError; // // let mut bytes = core::mem::take(&mut self.buffer); @@ -231,7 +231,7 @@ impl MemoryOutputPipe { #[async_trait::async_trait] impl HostOutputStream for MemoryOutputPipe { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), anyhow::Error> { // self.buffer.lock().unwrap().extend(buf); // Ok(buf.len() as u64) todo!() diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 86e34e10cd95..c1a71ecbb426 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -40,7 +40,7 @@ impl HostInputStream for EmptyStream { #[async_trait::async_trait] impl HostOutputStream for EmptyStream { - fn write(&mut self, buf: Bytes) -> Result { + fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { // Ok(buf.len() as u64) todo!() } diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 5a39eb968b32..b76a2c7461b3 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -52,7 +52,7 @@ pub trait HostInputStream: Send + Sync { pub trait HostOutputStream: Send + Sync { /// Write bytes. On success, returns the number of bytes written. /// Important: this write must be non-blocking! - fn write(&mut self, bytes: Bytes) -> Result; + fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error>; /// Transfer bytes directly from an input stream to an output stream. /// Important: this splice must be non-blocking! @@ -64,10 +64,11 @@ pub trait HostOutputStream: Send + Sync { let mut nspliced = 0; let mut state = StreamState::Open; + // TODO: handle the case where `bs.len()` is less than `nelem` let (bs, read_state) = src.read(nelem)?; // TODO: handle the case where write returns less than `bs.len()` - // TODO: handle the case where `bs.len()` is less than `nelem` - nspliced += self.write(bs)?; + let (nwritten, _write_state) = self.write(bs)?; + nspliced += nwritten; if read_state.is_closed() { state = read_state; } @@ -77,12 +78,12 @@ pub trait HostOutputStream: Send + Sync { /// Repeatedly write a byte to a stream. Important: this write must be /// non-blocking! - fn write_zeroes(&mut self, nelem: usize) -> Result { + fn write_zeroes(&mut self, nelem: usize) -> Result<(usize, StreamState), Error> { // TODO: We could optimize this to not allocate one big zeroed buffer, and instead write // repeatedly from a 'static buffer of zeros. let bs = Bytes::from_iter(core::iter::repeat(0 as u8).take(nelem)); - let nwritten = self.write(bs)?; - Ok(nwritten) + let r = self.write(bs)?; + Ok(r) } /// Check for write readiness: this method blocks until the stream is @@ -242,7 +243,7 @@ impl AsyncWriteStream { impl HostOutputStream for AsyncWriteStream { - fn write(&mut self, bytes: Bytes) -> Result { + fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { // let mut bytes = core::mem::take(&mut self.buffer); // bytes.extend(buf); // @@ -362,14 +363,17 @@ mod async_fd_stream { } async fn ready(&mut self) -> Result<(), Error> { + /* let _ = self.fd.readable().await?; Ok(()) + */ + todo!() } } #[async_trait::async_trait] impl HostOutputStream for AsyncFdStream { - fn write(&mut self, bytes: Bytes) -> Result { + fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error> { // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when // // we're done. // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; From 44e373ec9774866af09baaa304cc326731c35691 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 13 Jul 2023 15:31:57 -0700 Subject: [PATCH 063/118] hook new stream host read/write back up to the wit bindgen --- crates/wasi/src/preview2/preview2/io.rs | 31 +++++++++---------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 44b11969d996..1df842464eea 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -57,20 +57,12 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { - // let s = self.table_mut().get_input_stream_mut(stream)?; - // - // // Len could be any `u64` value, but we don't want to - // // allocate too much up front, so make a wild guess - // // of an upper bound for the buffer size. - // let buffer_len = std::cmp::min(len, 0x400000) as _; - // let mut buffer = vec![0; buffer_len]; - // - // let (bytes_read, state) = HostInputStream::read(s.as_mut(), &mut buffer)?; - // - // buffer.truncate(bytes_read as usize); - // - // Ok((buffer, state.is_closed())) - todo!() + let s = self.table_mut().get_input_stream_mut(stream)?; + + let (bytes, state) = HostInputStream::read(s.as_mut(), len as usize)?; + debug_assert!(bytes.len() <= len as usize); + + Ok((bytes.into(), state.into())) } async fn blocking_read( @@ -90,12 +82,11 @@ impl streams::Host for T { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - // let s = self.table_mut().get_output_stream_mut(stream)?; - // - // let bytes_written: u64 = HostOutputStream::write(s.as_mut(), &bytes)?; - // - // Ok(u64::try_from(bytes_written).unwrap()) - todo!() + let s = self.table_mut().get_output_stream_mut(stream)?; + + let (bytes_written, status) = HostOutputStream::write(s.as_mut(), bytes.into())?; + + Ok((u64::try_from(bytes_written).unwrap(), status.into())) } async fn blocking_write( From f01ee86f124feb91656630da55dadc95aaa6489d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 13 Jul 2023 15:32:22 -0700 Subject: [PATCH 064/118] sketchy AsyncReadStream impl --- crates/wasi/src/preview2/stream.rs | 151 +++++++++++++---------------- 1 file changed, 68 insertions(+), 83 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index b76a2c7461b3..fdad8591de03 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -136,88 +136,87 @@ impl TableStreamExt for Table { } /// Provides a [`HostInputStream`] impl from a [`tokio::io::AsyncRead`] impl -pub struct AsyncReadStream { +pub struct AsyncReadStream { state: StreamState, - buffer: Vec, - reader: T, + buffer: Option>, + receiver: tokio::sync::mpsc::Receiver>, } -impl AsyncReadStream { +impl AsyncReadStream { /// Create a [`AsyncReadStream`]. In order to use the [`HostInputStream`] impl /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. - pub fn new(reader: T) -> Self { + pub fn new(reader: T) -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(1); + tokio::spawn(async move { + let mut reader = reader; + loop { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = match reader.read_buf(&mut buf).await { + Ok(nbytes) if nbytes == 0 => { + sender.send(Ok((Bytes::new(), StreamState::Closed))).await + } + Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, + Err(e) => sender.send(Err(e)).await, + }; + if sent.is_err() { + // no more receiver - stop trying to read + break; + } + } + }); AsyncReadStream { state: StreamState::Open, - buffer: Vec::new(), - reader, + buffer: None, + receiver, } } } #[async_trait::async_trait] -impl HostInputStream - for AsyncReadStream -{ +impl HostInputStream for AsyncReadStream { fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // use std::io::Write; - // let l = dest.write(&self.buffer)?; - // - // self.buffer.drain(..l); - // if !self.buffer.is_empty() { - // return Ok((l as u64, StreamState::Open)); - // } - // - // if self.state.is_closed() { - // return Ok((l as u64, StreamState::Closed)); - // } - // - // let dest = &mut dest[l..]; - // let rest = if !dest.is_empty() { - // let mut readbuf = tokio::io::ReadBuf::new(dest); - // - // let noop_waker = noop_waker(); - // let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // // Make a synchronous, non-blocking call attempt to read. We are not - // // going to poll this more than once, so the noop waker is appropriate. - // match Pin::new(&mut self.reader).poll_read(&mut cx, &mut readbuf) { - // Poll::Pending => {} // Nothing was read - // Poll::Ready(result) => result?, // Maybe an error occured - // }; - // let bytes_read = readbuf.filled().len(); - // - // if bytes_read == 0 { - // self.state = StreamState::Closed; - // } - // bytes_read - // } else { - // 0 - // }; - // - // Ok(((l + rest) as u64, self.state)) - todo!() + use tokio::sync::mpsc::error::TryRecvError; + // FIXME: handle size argument! + match self.buffer.take() { + Some(Ok(bytes)) => return Ok((bytes, self.state)), + Some(Err(e)) => return Err(e.into()), + None => {} + } + + match self.receiver.try_recv() { + Ok(Ok((bytes, state))) => { + if state == StreamState::Closed { + self.state = state; + } + Ok((bytes, state)) + } + Ok(Err(e)) => Err(e.into()), + Err(TryRecvError::Empty) => Ok((Bytes::new(), self.state)), + Err(TryRecvError::Disconnected) => Err(anyhow::anyhow!( + "AsyncReadStream sender died - should be impossible" + )), + } } async fn ready(&mut self) -> Result<(), Error> { - if self.state.is_closed() { + if self.buffer.is_some() || self.state == StreamState::Closed { return Ok(()); } - - let mut bytes = core::mem::take(&mut self.buffer); - let start = bytes.len(); - bytes.resize(start + 1024, 0); - let l = - tokio::io::AsyncReadExt::read_buf(&mut self.reader, &mut &mut bytes[start..]).await?; - - // Reading 0 bytes means either there wasn't enough space in the buffer (which we - // know there is because we just resized) or that the stream has closed. Thus, we - // know the stream has closed here. - if l == 0 { - self.state = StreamState::Closed; + match self.receiver.recv().await { + Some(Ok((bytes, state))) => { + if state == StreamState::Closed { + self.state = state; + } + self.buffer = Some(Ok(bytes)); + } + Some(Err(e)) => self.buffer = Some(Err(e)), + None => { + return Err(anyhow::anyhow!( + "no more sender for an open AsyncReadStream - should be impossible" + )) + } } - - bytes.drain(start + l..); - self.buffer = bytes; - Ok(()) } } @@ -266,35 +265,18 @@ impl HostOutputStream } async fn ready(&mut self) -> Result<(), Error> { + /* use tokio::io::AsyncWriteExt; let bytes = core::mem::take(&mut self.buffer); if !bytes.is_empty() { self.writer.write_all(bytes.as_slice()).await?; } Ok(()) + */ + todo!() } } -// This implementation is basically copy-pasted out of `std` because the -// implementation there has not yet stabilized. When the `noop_waker` feature -// stabilizes, replace this with std::task::Waker::noop(). -fn noop_waker() -> Waker { - use std::task::{RawWaker, RawWakerVTable}; - const VTABLE: RawWakerVTable = RawWakerVTable::new( - // Cloning just returns a new no-op raw waker - |_| RAW, - // `wake` does nothing - |_| {}, - // `wake_by_ref` does nothing - |_| {}, - // Dropping does nothing as we don't allocate anything - |_| {}, - ); - const RAW: RawWaker = RawWaker::new(std::ptr::null(), &VTABLE); - - unsafe { Waker::from_raw(RAW) } -} - #[cfg(unix)] pub use async_fd_stream::*; @@ -393,8 +375,11 @@ mod async_fd_stream { } async fn ready(&mut self) -> Result<(), Error> { + /* let _ = self.fd.writable().await?; Ok(()) + */ + todo!() } } } From fc4652c28204faed906b4594980d2ec14fba5c67 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Thu, 13 Jul 2023 17:18:47 -0700 Subject: [PATCH 065/118] Fill out implementations for AsyncReadStream and AsyncWriteStream --- crates/wasi/Cargo.toml | 3 + crates/wasi/src/preview2/stdio.rs | 4 +- crates/wasi/src/preview2/stream.rs | 226 +++++++++++++++++++++++------ 3 files changed, 183 insertions(+), 50 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 0c28f707740a..024726dfb3df 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -36,6 +36,9 @@ async-trait = { workspace = true, optional = true } system-interface = { workspace = true, optional = true} futures-util = { workspace = true, optional = true } +[dev-dependencies] +tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros"] } + [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["fs"], optional = true } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index c1a71ecbb426..196e1181aa4d 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -13,12 +13,12 @@ mod windows; #[cfg(windows)] pub use self::windows::{stdin, Stdin}; -pub type Stdout = AsyncWriteStream; +pub type Stdout = AsyncWriteStream; pub fn stdout() -> Stdout { AsyncWriteStream::new(tokio::io::stdout()) } -pub type Stderr = AsyncWriteStream; +pub type Stderr = AsyncWriteStream; pub fn stderr() -> Stderr { AsyncWriteStream::new(tokio::io::stderr()) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index fdad8591de03..ef3619332840 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -145,10 +145,9 @@ pub struct AsyncReadStream { impl AsyncReadStream { /// Create a [`AsyncReadStream`]. In order to use the [`HostInputStream`] impl /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. - pub fn new(reader: T) -> Self { + pub fn new(mut reader: T) -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { - let mut reader = reader; loop { use tokio::io::AsyncReadExt; let mut buf = bytes::BytesMut::with_capacity(4096); @@ -177,19 +176,38 @@ impl AsyncReadStream { impl HostInputStream for AsyncReadStream { fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { use tokio::sync::mpsc::error::TryRecvError; - // FIXME: handle size argument! + match self.buffer.take() { - Some(Ok(bytes)) => return Ok((bytes, self.state)), + Some(Ok(mut bytes)) => { + // TODO: de-duplicate the buffer management with the case below + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + let return_state = if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + StreamState::Open + } else { + self.state + }; + return Ok((bytes, return_state)); + } Some(Err(e)) => return Err(e.into()), None => {} } - match self.receiver.try_recv() { - Ok(Ok((bytes, state))) => { - if state == StreamState::Closed { - self.state = state; - } - Ok((bytes, state)) + match dbg!(self.receiver.try_recv()) { + Ok(Ok((mut bytes, state))) => { + self.state = state; + + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + let return_state = if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + StreamState::Open + } else { + self.state + }; + + Ok((bytes, return_state)) } Ok(Err(e)) => Err(e.into()), Err(TryRecvError::Empty) => Ok((Bytes::new(), self.state)), @@ -221,59 +239,141 @@ impl HostInputStream for AsyncReadStream { } } +enum WriteState { + Ready, + Pending, + Err(std::io::Error), +} + /// Provides a [`HostOutputStream`] impl from a [`tokio::io::AsyncWrite`] impl -pub struct AsyncWriteStream { - buffer: Vec, - writer: T, +pub struct AsyncWriteStream { + state: Option, + sender: tokio::sync::mpsc::Sender, + result_receiver: tokio::sync::mpsc::Receiver>, } -impl AsyncWriteStream { +impl AsyncWriteStream { /// Create a [`AsyncWriteStream`]. In order to use the [`HostOutputStream`] impl /// provided by this struct, the argument must impl [`tokio::io::AsyncWrite`]. - pub fn new(writer: T) -> Self { + pub fn new(mut writer: T) -> Self { + let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); + let (result_sender, result_receiver) = tokio::sync::mpsc::channel(1); + + tokio::spawn(async move { + 'outer: loop { + use tokio::io::AsyncWriteExt; + match receiver.recv().await { + Some(mut bytes) => { + while !bytes.is_empty() { + match writer.write_buf(&mut bytes).await { + Ok(0) => { + let _ = result_sender.send(Ok(StreamState::Closed)).await; + break 'outer; + } + + Ok(_) => { + if bytes.is_empty() { + match result_sender.send(Ok(StreamState::Open)).await { + Ok(_) => break, + Err(_) => break 'outer, + } + } + continue; + } + + Err(e) => match result_sender.send(Err(e)).await { + Ok(_) => break, + Err(_) => break 'outer, + }, + } + } + } + + // The other side of the channel hung up, the task can exit now + None => break 'outer, + } + } + }); + AsyncWriteStream { - buffer: Vec::new(), - writer, + state: Some(WriteState::Ready), + sender, + result_receiver, } } } #[async_trait::async_trait] -impl HostOutputStream - for AsyncWriteStream -{ +impl HostOutputStream for AsyncWriteStream { fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { - // let mut bytes = core::mem::take(&mut self.buffer); - // bytes.extend(buf); - // - // let noop_waker = noop_waker(); - // let mut cx: Context<'_> = Context::from_waker(&noop_waker); - // // Make a synchronous, non-blocking call attempt to write. We are not - // // going to poll this more than once, so the noop waker is appropriate. - // match Pin::new(&mut self.writer).poll_write(&mut cx, &mut bytes.as_slice()) { - // Poll::Pending => { - // // Nothing was written: buffer all of it below. - // } - // Poll::Ready(written) => { - // // So much was written: - // bytes.drain(..written?); - // } - // } - // self.buffer = bytes; - // Ok(buf.len() as u64) - todo!() + use tokio::sync::mpsc::error::{TryRecvError, TrySendError}; + + match self.state.take() { + Some(WriteState::Ready) => {} + Some(WriteState::Pending) => match self.result_receiver.try_recv() { + Ok(Ok(StreamState::Open)) => { + self.state = Some(WriteState::Ready); + } + + Ok(Ok(StreamState::Closed)) => { + self.state = None; + return Ok((0, StreamState::Closed)); + } + + Ok(Err(e)) => { + self.state = Some(WriteState::Ready); + return Err(e.into()); + } + + Err(TryRecvError::Empty) => { + self.state = Some(WriteState::Pending); + return Ok((0, StreamState::Open)); + } + + Err(TryRecvError::Disconnected) => { + unreachable!("task shouldn't die while pending") + } + }, + Some(WriteState::Err(e)) => { + self.state = Some(WriteState::Ready); + return Err(e.into()); + } + + None => { + return Ok((0, StreamState::Closed)); + } + } + + let len = bytes.len(); + match self.sender.try_send(bytes) { + Ok(_) => Ok((len, StreamState::Open)), + Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), + Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), + } } async fn ready(&mut self) -> Result<(), Error> { - /* - use tokio::io::AsyncWriteExt; - let bytes = core::mem::take(&mut self.buffer); - if !bytes.is_empty() { - self.writer.write_all(bytes.as_slice()).await?; + match &self.state { + Some(WriteState::Pending) => match self.result_receiver.recv().await { + Some(Ok(StreamState::Open)) => { + self.state = Some(WriteState::Ready); + } + + Some(Ok(StreamState::Closed)) => { + self.state = None; + } + + Some(Err(e)) => { + self.state = Some(WriteState::Err(e)); + } + + None => unreachable!("task shouldn't die while pending"), + }, + + Some(WriteState::Ready | WriteState::Err(_)) | None => {} } + Ok(()) - */ - todo!() } } @@ -392,7 +492,7 @@ mod test { struct DummyInputStream; #[async_trait::async_trait] impl HostInputStream for DummyInputStream { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { + fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { @@ -413,7 +513,7 @@ mod test { struct DummyOutputStream; #[async_trait::async_trait] impl HostOutputStream for DummyOutputStream { - fn write(&mut self, _: Bytes) -> Result { + fn write(&mut self, _: Bytes) -> Result<(usize, StreamState), Error> { unimplemented!(); } async fn ready(&mut self) -> Result<(), Error> { @@ -428,4 +528,34 @@ mod test { let ix = table.push_output_stream(Box::new(dummy)).unwrap(); let _ = table.get_output_stream_mut(ix).unwrap(); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_empty_read_stream() { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = tokio::io::empty().read_buf(&mut buf).await.unwrap(); + assert_eq!(sent, 0); + + let mut reader = AsyncReadStream::new(tokio::io::empty()); + let (bs, state) = reader.read(10).unwrap(); + assert!(bs.is_empty()); + + // In a multi-threaded context, the value of state is not deterministic -- the spawned + // reader task may run on a different thread. + match state { + // The reader task ran before we tried to read, and noticed that the input was empty. + StreamState::Closed => {} + + // The reader task hasn't run yet, and we need to call `ready` to turn the crank. + StreamState::Open => { + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + let (bs, state) = reader.read(10).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Closed); + } + } + } } From 6b3ce6a530d06783625b7ce7b891ea5401640ab5 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 11:13:08 -0700 Subject: [PATCH 066/118] some reasonable read tests --- crates/wasi/src/preview2/stream.rs | 90 +++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index ef3619332840..58fec07231ca 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -487,6 +487,7 @@ mod async_fd_stream { #[cfg(test)] mod test { use super::*; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; #[test] fn input_stream_in_table() { struct DummyInputStream; @@ -530,12 +531,7 @@ mod test { } #[tokio::test(flavor = "multi_thread")] - async fn test_empty_read_stream() { - use tokio::io::AsyncReadExt; - let mut buf = bytes::BytesMut::with_capacity(4096); - let sent = tokio::io::empty().read_buf(&mut buf).await.unwrap(); - assert_eq!(sent, 0); - + async fn empty_read_stream() { let mut reader = AsyncReadStream::new(tokio::io::empty()); let (bs, state) = reader.read(10).unwrap(); assert!(bs.is_empty()); @@ -546,16 +542,94 @@ mod test { // The reader task ran before we tried to read, and noticed that the input was empty. StreamState::Closed => {} - // The reader task hasn't run yet, and we need to call `ready` to turn the crank. + // The reader task hasn't run yet. Call `ready` to await and fill the buffer. StreamState::Open => { tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); - let (bs, state) = reader.read(10).unwrap(); + let (bs, state) = reader.read(0).unwrap(); assert!(bs.is_empty()); assert_eq!(state, StreamState::Closed); } } } + + #[tokio::test(flavor = "multi_thread")] + async fn infinite_read_stream() { + let mut reader = AsyncReadStream::new(tokio::io::repeat(0)); + + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(bs.len(), 10); + assert_eq!(state, StreamState::Open); + } else { + assert_eq!(bs.len(), 10); + } + + // Subsequent reads should succeed + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(state, StreamState::Open); + assert_eq!(bs.len(), 10); + + // Even 0-length reads should succeed and show its open + let (bs, state) = reader.read(0).unwrap(); + assert_eq!(state, StreamState::Open); + assert_eq!(bs.len(), 0); + } + + async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + Sync + 'static { + let (mut a, b) = tokio::io::duplex(contents.len()); + a.write_all(contents).await.unwrap(); + let (read_half, _write_half) = tokio::io::split(b); + read_half + } + + #[tokio::test(flavor = "multi_thread")] + async fn finite_read_stream() { + let mut reader = AsyncReadStream::new(finite_async_reader(&[1; 123]).await); + + let (bs, state) = reader.read(123).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(123).unwrap(); + assert_eq!(bs.len(), 123); + assert_eq!(state, StreamState::Open); + } else { + assert_eq!(bs.len(), 123); + } + + // The AsyncRead's should be empty now, but we have a race where the reader task hasn't + // yet send that to the AsyncReadStream. + let (bs, state) = reader.read(0).unwrap(); + assert!(bs.is_empty()); + match state { + StreamState::Closed => {} // Correct! + StreamState::Open => { + // Need to await to give this side time to catch up + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should show closed + let (bs, state) = reader.read(0).unwrap(); + assert_eq!(bs.len(), 0); + assert_eq!(state, StreamState::Closed); + } + } + } } From 276fdfc250ce548f6c7739c934b134be4a2be6b2 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 11:47:19 -0700 Subject: [PATCH 067/118] more --- crates/wasi/src/preview2/stream.rs | 105 +++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 58fec07231ca..7c47a434fd31 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -586,11 +586,22 @@ mod test { assert_eq!(bs.len(), 0); } - async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + Sync + 'static { - let (mut a, b) = tokio::io::duplex(contents.len()); - a.write_all(contents).await.unwrap(); + fn simplex( + size: usize, + ) -> ( + impl AsyncRead + Send + Sync + 'static, + impl AsyncWrite + Send + Sync + 'static, + ) { + let (a, b) = tokio::io::duplex(size); + let (_read_half, write_half) = tokio::io::split(a); let (read_half, _write_half) = tokio::io::split(b); - read_half + (read_half, write_half) + } + + async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + Sync + 'static { + let (r, mut w) = simplex(contents.len()); + w.write_all(contents).await.unwrap(); + r } #[tokio::test(flavor = "multi_thread")] @@ -632,4 +643,90 @@ mod test { } } } + + #[tokio::test(flavor = "multi_thread")] + async fn multiple_chunks_read_stream() { + let (r, mut w) = simplex(1024); + let mut reader = AsyncReadStream::new(r); + + w.write_all(&[123]).await.unwrap(); + + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(*bs, [123u8]); + assert_eq!(state, StreamState::Open); + } else { + assert_eq!(*bs, [123u8]); + } + + // The stream should be empty and open now: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // We can wait on readiness and it will time out: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .err() + .expect("the reader should time out"); + + // Still open and empty: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // Put something else in the stream: + w.write_all(&[45]).await.unwrap(); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("the ready is ok"); + + // read the something else back out: + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(*bs, [45u8]); + assert_eq!(state, StreamState::Open); + + // nothing else in there: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // We can wait on readiness and it will time out: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .err() + .expect("the reader should time out"); + + // nothing else in there: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // Now close the pipe: + drop(w); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("the ready is ok"); + + // empty and now closed: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Closed); + } } From 63d346e181ff9b7b69837183ed42f542b06023eb Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 12:38:00 -0700 Subject: [PATCH 068/118] first smoke test for AsyncWriteStream --- crates/wasi/src/preview2/stream.rs | 136 ++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 20 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 7c47a434fd31..04282ea4f639 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -239,6 +239,7 @@ impl HostInputStream for AsyncReadStream { } } +#[derive(Debug)] enum WriteState { Ready, Pending, @@ -301,54 +302,66 @@ impl AsyncWriteStream { result_receiver, } } + + fn send(&mut self, bytes: Bytes) -> anyhow::Result<(usize, StreamState)> { + use tokio::sync::mpsc::error::TrySendError; + + debug_assert!(matches!(self.state, Some(WriteState::Ready))); + + let len = bytes.len(); + match dbg!(self.sender.try_send(bytes)) { + Ok(_) => { + self.state = Some(WriteState::Pending); + Ok((len, StreamState::Open)) + } + Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), + Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), + } + } } #[async_trait::async_trait] impl HostOutputStream for AsyncWriteStream { fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { - use tokio::sync::mpsc::error::{TryRecvError, TrySendError}; + use tokio::sync::mpsc::error::TryRecvError; - match self.state.take() { - Some(WriteState::Ready) => {} - Some(WriteState::Pending) => match self.result_receiver.try_recv() { + match self.state { + Some(WriteState::Ready) => self.send(bytes), + Some(WriteState::Pending) => match dbg!(self.result_receiver.try_recv()) { Ok(Ok(StreamState::Open)) => { self.state = Some(WriteState::Ready); + self.send(bytes) } Ok(Ok(StreamState::Closed)) => { self.state = None; - return Ok((0, StreamState::Closed)); + Ok((0, StreamState::Closed)) } Ok(Err(e)) => { self.state = Some(WriteState::Ready); - return Err(e.into()); + Err(e.into()) } Err(TryRecvError::Empty) => { self.state = Some(WriteState::Pending); - return Ok((0, StreamState::Open)); + Ok((0, StreamState::Open)) } Err(TryRecvError::Disconnected) => { unreachable!("task shouldn't die while pending") } }, - Some(WriteState::Err(e)) => { - self.state = Some(WriteState::Ready); - return Err(e.into()); + Some(WriteState::Err(_)) => { + // Move the error payload out of self.state, because errors are not Copy + if let Some(WriteState::Err(e)) = self.state.replace(WriteState::Ready) { + Err(e.into()) + } else { + unreachable!("self.state shown to be Some(Err(e)) in match clause") + } } - None => { - return Ok((0, StreamState::Closed)); - } - } - - let len = bytes.len(); - match self.sender.try_send(bytes) { - Ok(_) => Ok((len, StreamState::Open)), - Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), - Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), + None => Ok((0, StreamState::Closed)), } } @@ -729,4 +742,87 @@ mod test { assert!(bs.is_empty()); assert_eq!(state, StreamState::Closed); } + + #[tokio::test(flavor = "multi_thread")] + // At the moment we are restricting AsyncReadStream from buffering more than 4k. This isn't a + // suitable design for all applications, and we will probably make a knob or change the + // behavior at some point, but this test shows the behavior as it is implemented: + async fn backpressure_read_stream() { + let (r, mut w) = simplex(16 * 1024); // Make sure this buffer isnt a bottleneck + let mut reader = AsyncReadStream::new(r); + + let writer_task = tokio::task::spawn(async move { + // Write twice as much as we can buffer up in an AsyncReadStream: + w.write_all(&[123; 8192]).await.unwrap(); + w + }); + + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Now we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + assert_eq!(state, StreamState::Open); + + // Allow the crank to turn more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Again we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + assert_eq!(state, StreamState::Open); + + // The writer task is now finished - join with it: + let w = tokio::time::timeout(std::time::Duration::from_millis(10), writer_task) + .await + .expect("the join should be ready instantly"); + // And close the pipe: + drop(w); + + // Allow the crank to turn more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Now we expect the reader to be empty, and the stream closed: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 0); + assert_eq!(state, StreamState::Closed); + } + + #[tokio::test(flavor = "multi_thread")] + async fn sink_write_stream() { + let mut writer = AsyncWriteStream::new(tokio::io::sink()); + let chunk = Bytes::from_static(&[0; 1024]); + + // I can write whatever: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + println!("second write:"); + // But I expect this to block additional writes: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // Now additional writes will work: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + } } From 9ec8f554fec86fa1e45d9a1e57416c09164e2d4c Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 15:21:38 -0700 Subject: [PATCH 069/118] bunch of AsyncWriteStream tests --- crates/wasi/src/preview2/stream.rs | 170 ++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 13 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 04282ea4f639..f3fcd5359e64 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -194,7 +194,7 @@ impl HostInputStream for AsyncReadStream { None => {} } - match dbg!(self.receiver.try_recv()) { + match self.receiver.try_recv() { Ok(Ok((mut bytes, state))) => { self.state = state; @@ -271,7 +271,6 @@ impl AsyncWriteStream { let _ = result_sender.send(Ok(StreamState::Closed)).await; break 'outer; } - Ok(_) => { if bytes.is_empty() { match result_sender.send(Ok(StreamState::Open)).await { @@ -281,11 +280,10 @@ impl AsyncWriteStream { } continue; } - - Err(e) => match result_sender.send(Err(e)).await { - Ok(_) => break, - Err(_) => break 'outer, - }, + Err(e) => { + let _ = result_sender.send(Err(e)).await; + break 'outer; + } } } } @@ -309,7 +307,7 @@ impl AsyncWriteStream { debug_assert!(matches!(self.state, Some(WriteState::Ready))); let len = bytes.len(); - match dbg!(self.sender.try_send(bytes)) { + match self.sender.try_send(bytes) { Ok(_) => { self.state = Some(WriteState::Pending); Ok((len, StreamState::Open)) @@ -325,9 +323,10 @@ impl HostOutputStream for AsyncWriteStream { fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { use tokio::sync::mpsc::error::TryRecvError; + dbg!(&self.state); match self.state { Some(WriteState::Ready) => self.send(bytes), - Some(WriteState::Pending) => match dbg!(self.result_receiver.try_recv()) { + Some(WriteState::Pending) => match self.result_receiver.try_recv() { Ok(Ok(StreamState::Open)) => { self.state = Some(WriteState::Ready); self.send(bytes) @@ -339,7 +338,7 @@ impl HostOutputStream for AsyncWriteStream { } Ok(Err(e)) => { - self.state = Some(WriteState::Ready); + self.state = None; Err(e.into()) } @@ -353,8 +352,9 @@ impl HostOutputStream for AsyncWriteStream { } }, Some(WriteState::Err(_)) => { - // Move the error payload out of self.state, because errors are not Copy - if let Some(WriteState::Err(e)) = self.state.replace(WriteState::Ready) { + // Move the error payload out of self.state, because errors are not Copy, + // and set self.state to None, because the stream is now closed. + if let Some(WriteState::Err(e)) = self.state.take() { Err(e.into()) } else { unreachable!("self.state shown to be Some(Err(e)) in match clause") @@ -658,6 +658,8 @@ mod test { } #[tokio::test(flavor = "multi_thread")] + // Test that you can write items into the stream, and they get read out in the order they were + // written, with the proper indications of readiness for reading: async fn multiple_chunks_read_stream() { let (r, mut w) = simplex(1024); let mut reader = AsyncReadStream::new(r); @@ -809,7 +811,6 @@ mod test { assert_eq!(len, chunk.len()); assert_eq!(state, StreamState::Open); - println!("second write:"); // But I expect this to block additional writes: let (len, state) = writer.write(chunk.clone()).unwrap(); assert_eq!(len, 0); @@ -825,4 +826,147 @@ mod test { assert_eq!(len, chunk.len()); assert_eq!(state, StreamState::Open); } + + #[tokio::test(flavor = "multi_thread")] + async fn closed_write_stream() { + let (reader, writer) = simplex(1024); + drop(reader); + let mut writer = AsyncWriteStream::new(writer); + + // Without checking write readiness, perform a nonblocking write: this should succeed + // because we will buffer up the write. + let chunk = Bytes::from_static(&[0; 1]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // Check write readiness: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // When we drop the simplex reader, that causes the simplex writer to return BrokenPipe on + // its write. Now that the buffering crank has turned, our next write will give BrokenPipe. + let err = writer.write(chunk.clone()).err().unwrap(); + assert_eq!( + err.downcast_ref::().unwrap().kind(), + std::io::ErrorKind::BrokenPipe + ); + + // Now that we got the error out of the writer, it should be closed - subsequent writes + // will not work + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Closed); + } + + #[tokio::test(flavor = "multi_thread")] + async fn multiple_chunks_write_stream() { + use std::ops::Deref; + + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(writer); + + // Write a chunk: + let chunk = Bytes::from_static(&[123; 1]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // After the write, still ready for more writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + let mut read_buf = vec![0; chunk.len()]; + let read_len = reader.read_exact(&mut read_buf).await.unwrap(); + assert_eq!(read_len, chunk.len()); + assert_eq!(read_buf.as_slice(), chunk.deref()); + + // Write a second, different chunk: + let chunk2 = Bytes::from_static(&[45; 1]); + let (len, state) = writer.write(chunk2.clone()).unwrap(); + assert_eq!(len, chunk2.len()); + assert_eq!(state, StreamState::Open); + + // After the write, still ready for more writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + let mut read2_buf = vec![0; chunk2.len()]; + let read2_len = reader.read_exact(&mut read2_buf).await.unwrap(); + assert_eq!(read2_len, chunk2.len()); + assert_eq!(read2_buf.as_slice(), chunk2.deref()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn backpressure_write_stream() { + // Stream can buffer up to 1k, plus one write chunk, before not + // accepting more input: + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(writer); + + // Write enough to fill the simplex buffer: + let chunk = Bytes::from_static(&[0; 1024]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // turn the crank and it should be ready for writing again: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // Now fill the buffer between here and the writer task: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // Try shoving even more down there, and it shouldnt accept more input: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + // turn the crank and it should Not become ready for writing until we read something out. + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .err() + .expect("the writer should be not become ready"); + + // Still not ready from the .write interface either: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + // There is 2k in the buffer. I should be able to read all of it out: + let mut buf = [0; 2048]; + reader.read_exact(&mut buf).await.unwrap(); + + // and no more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.read(&mut buf)) + .await + .err() + .expect("nothing more buffered in the system"); + + // Now the backpressure should be cleared, and an additional write should be accepted. + + // immediately ready for writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // and the write succeeds: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + } } From 3eb762e3330a7228318bfe01296483b52d0fdc16 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 15:21:52 -0700 Subject: [PATCH 070/118] half-baked idea that the output-stream interface will need a flush mechanism --- crates/wasi/wit/deps/io/streams.wit | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi/wit/deps/io/streams.wit index 0943759ab32e..46cee73dc5f6 100644 --- a/crates/wasi/wit/deps/io/streams.wit +++ b/crates/wasi/wit/deps/io/streams.wit @@ -163,6 +163,25 @@ interface streams { buf: list ) -> result, stream-error> + /* FIXME: i think we need some nonsense like this, because a nonblocking + * write is necessarily buffering the caller's input somewhere, and + * the caller may need to flush (block until buffer has been emptied) any + * buffers we create on its behalf. + * however this is a whole can of worms. + write-with-flush: func( + this: output-stream, + /// Data to write + buf: list + ) -> result + + // returns Some once ready: + flush-check: func(this: flush-resource) -> option, stream-error>> + // get a pollable to check readiness: + subscribe-to-flush: func(this: flush-resource) -> pollable + // drop the resource: + flush-delete: func(this: flush-resource) + */ + /// Write bytes to a stream, with blocking. /// /// This is similar to `write`, except that it blocks until at least one From 22e74d2ee8353a25fdbba5e36bde1dd2b7951147 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 16:44:04 -0700 Subject: [PATCH 071/118] adapter: fixes for changes to stream wit --- crates/wasi-preview1-component-adapter/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs index d3f1ffa7710a..358ef3584057 100644 --- a/crates/wasi-preview1-component-adapter/src/lib.rs +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -839,7 +839,7 @@ pub unsafe extern "C" fn fd_read( let read_len = u64::try_from(len).trapping_unwrap(); let wasi_stream = streams.get_read_stream()?; - let (data, end) = state + let (data, stream_stat) = state .import_alloc .with_buffer(ptr, len, || { if blocking { @@ -861,7 +861,7 @@ pub unsafe extern "C" fn fd_read( let len = data.len(); forget(data); - if !end && len == 0 { + if stream_stat == crate::streams::StreamStatus::Open && len == 0 { Err(ERRNO_INTR) } else { *nread = len; @@ -1215,7 +1215,7 @@ pub unsafe extern "C" fn fd_write( Descriptor::Streams(streams) => { let wasi_stream = streams.get_write_stream()?; - let bytes = if let StreamType::File(file) = &streams.type_ { + let (bytes, _stream_stat) = if let StreamType::File(file) = &streams.type_ { if file.blocking { streams::blocking_write(wasi_stream, bytes) } else { From 95330391d9bbafa9da42659889af1a36c3bf8115 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 16:44:45 -0700 Subject: [PATCH 072/118] fix new rust 1.71 warnings --- crates/wasi-preview1-component-adapter/src/descriptors.rs | 2 +- crates/wasi-preview1-component-adapter/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wasi-preview1-component-adapter/src/descriptors.rs b/crates/wasi-preview1-component-adapter/src/descriptors.rs index de8b3a13cf7f..2a0f7928b832 100644 --- a/crates/wasi-preview1-component-adapter/src/descriptors.rs +++ b/crates/wasi-preview1-component-adapter/src/descriptors.rs @@ -305,7 +305,7 @@ impl Descriptors { // Implementation of fd_renumber pub fn renumber(&mut self, from_fd: Fd, to_fd: Fd) -> Result<(), Errno> { // First, ensure from_fd is in bounds: - drop(self.get(from_fd)?); + let _ = self.get(from_fd)?; // Expand table until to_fd is in bounds as well: while self.table_len.get() as u32 <= to_fd as u32 { self.push_closed()?; diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs index 358ef3584057..32c5b36d05f3 100644 --- a/crates/wasi-preview1-component-adapter/src/lib.rs +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -1376,7 +1376,7 @@ pub unsafe extern "C" fn path_open( fdflags: Fdflags, opened_fd: *mut Fd, ) -> Errno { - drop(fs_rights_inheriting); + let _ = fs_rights_inheriting; let path = slice::from_raw_parts(path_ptr, path_len); let at_flags = at_flags_from_lookupflags(dirflags); From 189fcf967b00de13385ad1a0cf35f920c0e6db21 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 17:05:59 -0700 Subject: [PATCH 073/118] make stdin work on unix without using AsyncFdStream inline the tokio docs example of how to impl AsyncRead for an AsyncFd, except theres some "minor" changes because stdin doesnt impl Read on &Stdin whereas tcpstream from the example does --- Cargo.lock | 24 +++++++- Cargo.toml | 2 +- crates/wasi/Cargo.toml | 4 +- crates/wasi/src/preview2/mod.rs | 33 ++++++---- crates/wasi/src/preview2/pipe.rs | 4 +- crates/wasi/src/preview2/stdio.rs | 7 +-- crates/wasi/src/preview2/stdio/unix.rs | 75 +++++++++++++++++++---- crates/wasi/src/preview2/stream.rs | 84 +++++++++++++------------- 8 files changed, 159 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d51cecae9a05..a7c534bffeca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1376,6 +1376,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "futures" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.27" @@ -1383,6 +1397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1391,6 +1406,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + [[package]] name = "futures-sink" version = "0.3.27" @@ -1410,6 +1431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", @@ -4236,7 +4258,7 @@ dependencies = [ "cap-std", "cap-time-ext", "fs-set-times", - "futures-util", + "futures", "io-extras", "libc", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 96b0ffd3cdb9..4d07335a32e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,7 +241,7 @@ libc = "0.2.60" file-per-thread-logger = "0.2.0" tokio = { version = "1.26.0" } bytes = "1.4" -futures-util = { version = "0.3.27", default-features = false } +futures = { version = "0.3.27", default-features = false } [features] default = [ diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 024726dfb3df..c7d1ac79ce05 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -34,7 +34,7 @@ fs-set-times = { workspace = true, optional = true } bitflags = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } system-interface = { workspace = true, optional = true} -futures-util = { workspace = true, optional = true } +futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros"] } @@ -66,7 +66,7 @@ preview2 = [ 'dep:system-interface', 'dep:rustix', 'dep:tokio', - 'dep:futures-util', + 'dep:futures', ] preview1-on-preview2 = [ "preview2", diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 31feaad1b60c..1f657c1f2104 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -36,8 +36,6 @@ pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; pub use self::random::{thread_rng, Deterministic}; -#[cfg(unix)] -pub use self::stream::AsyncFdStream; pub use self::stream::{ AsyncReadStream, AsyncWriteStream, HostInputStream, HostOutputStream, StreamState, TableStreamExt, @@ -127,22 +125,33 @@ pub mod bindings { pub use self::_internal_rest::wasi::*; } +static RUNTIME: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_time() + .enable_io() + .build() + .unwrap() +}); + +pub(crate) fn in_tokio G>(f: F) -> G { + match tokio::runtime::Handle::try_current() { + Ok(_) => f(), + Err(_) => { + let _enter = RUNTIME.enter(); + RUNTIME.block_on(async { f() }) + } + } +} + +// FIXME: this can maybe be written in terms of `in_tokio` but i have broken my tools with my +// tools right now and cant trust my tests pub(crate) fn block_on(f: F) -> F::Output { - use tokio::runtime::{Builder, Handle, Runtime}; - match Handle::try_current() { + match tokio::runtime::Handle::try_current() { Ok(h) => { let _enter = h.enter(); h.block_on(f) } Err(_) => { - use once_cell::sync::Lazy; - static RUNTIME: Lazy = Lazy::new(|| { - Builder::new_multi_thread() - .enable_time() - .enable_io() - .build() - .unwrap() - }); let _enter = RUNTIME.enter(); RUNTIME.block_on(f) } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 5e43ffdefa7f..c2f1b78e4339 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -202,7 +202,9 @@ impl HostInputStream for MemoryInputPipe { if self.buffer.get_ref().len() as u64 > self.buffer.position() { Ok(()) } else { - futures_util::future::pending().await + // FIXME + //futures_util::future::pending().await + todo!() } } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 196e1181aa4d..9ed36491cbf6 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -28,13 +28,12 @@ pub struct EmptyStream; #[async_trait::async_trait] impl HostInputStream for EmptyStream { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // Ok((0, StreamState::Open)) - todo!() + fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { + Ok((Bytes::new(), StreamState::Open)) } async fn ready(&mut self) -> Result<(), Error> { - futures_util::future::pending().await + futures::future::pending().await } } diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index 2ade52dd726d..412edd220f16 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -1,6 +1,7 @@ -use crate::preview2::{AsyncFdStream, HostInputStream, StreamState}; +use crate::preview2::{AsyncReadStream, StreamState}; use anyhow::Error; use bytes::Bytes; +use tokio::io::unix::AsyncFd; // wasmtime cant use std::sync::OnceLock yet because of a llvm regression in // 1.70. when 1.71 is released, we can switch to using std here. @@ -13,11 +14,11 @@ use tokio::sync::Mutex; // We need a single global instance of the AsyncFd because creating // this instance registers the process's stdin fd with epoll, which will // return an error if an fd is registered more than once. -type GlobalStdin = Mutex>; +type GlobalStdin = Mutex; static STDIN: OnceLock = OnceLock::new(); fn create() -> anyhow::Result { - Ok(Mutex::new(AsyncFdStream::new(std::io::stdin())?)) + Ok(Mutex::new(AsyncReadStream::new(InnerStdin::new()?))) } pub struct Stdin; @@ -42,19 +43,69 @@ pub fn stdin() -> Stdin { } #[async_trait::async_trait] -impl HostInputStream for Stdin { +impl crate::preview2::HostInputStream for Stdin { fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // let mut r = move || Self::get_global().blocking_lock().read(buf); - // // If we are currently in a tokio context, blocking_lock will panic unless inside a - // // block_in_place: - // match tokio::runtime::Handle::try_current() { - // Ok(_) => tokio::task::block_in_place(r), - // Err(_) => r(), - // } - todo!() + let r = move || Self::get_global().blocking_lock().read(size); + // If we are currently in a tokio context, blocking_lock will panic unless inside a + // block_in_place: + match tokio::runtime::Handle::try_current() { + Ok(_) => tokio::task::block_in_place(r), + Err(_) => r(), + } } async fn ready(&mut self) -> Result<(), Error> { Self::get_global().lock().await.ready().await } } + +struct InnerStdin { + inner: AsyncFd, +} + +impl InnerStdin { + pub fn new() -> anyhow::Result { + use rustix::fs::OFlags; + use std::os::fd::AsRawFd; + + let stdin = std::io::stdin(); + + let borrowed_fd = unsafe { rustix::fd::BorrowedFd::borrow_raw(stdin.as_raw_fd()) }; + let flags = rustix::fs::fcntl_getfl(borrowed_fd)?; + if !flags.contains(OFlags::NONBLOCK) { + rustix::fs::fcntl_setfl(borrowed_fd, flags.difference(OFlags::NONBLOCK))?; + } + + Ok(Self { + inner: AsyncFd::new(std::io::stdin())?, + }) + } +} + +use futures::ready; +use std::io::{self, Read}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, ReadBuf}; + +impl AsyncRead for InnerStdin { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + loop { + let mut guard = ready!(self.inner.poll_read_ready_mut(cx))?; + + let unfilled = buf.initialize_unfilled(); + match guard.try_io(|inner| inner.get_mut().read(unfilled)) { + Ok(Ok(len)) => { + buf.advance(len); + return Poll::Ready(Ok(())); + } + Ok(Err(err)) => return Poll::Ready(Err(err)), + Err(_would_block) => continue, + } + } + } +} diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index f3fcd5359e64..c2771ea858c2 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,8 +1,6 @@ use crate::preview2::{Table, TableError}; use anyhow::Error; use bytes::Bytes; -use std::pin::Pin; -use std::task::{Context, Poll, Waker}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum StreamState { @@ -147,22 +145,24 @@ impl AsyncReadStream { /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. pub fn new(mut reader: T) -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(1); - tokio::spawn(async move { - loop { - use tokio::io::AsyncReadExt; - let mut buf = bytes::BytesMut::with_capacity(4096); - let sent = match reader.read_buf(&mut buf).await { - Ok(nbytes) if nbytes == 0 => { - sender.send(Ok((Bytes::new(), StreamState::Closed))).await + crate::preview2::in_tokio(|| { + tokio::spawn(async move { + loop { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = match reader.read_buf(&mut buf).await { + Ok(nbytes) if nbytes == 0 => { + sender.send(Ok((Bytes::new(), StreamState::Closed))).await + } + Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, + Err(e) => sender.send(Err(e)).await, + }; + if sent.is_err() { + // no more receiver - stop trying to read + break; } - Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, - Err(e) => sender.send(Err(e)).await, - }; - if sent.is_err() { - // no more receiver - stop trying to read - break; } - } + }) }); AsyncReadStream { state: StreamState::Open, @@ -260,38 +260,40 @@ impl AsyncWriteStream { let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); let (result_sender, result_receiver) = tokio::sync::mpsc::channel(1); - tokio::spawn(async move { - 'outer: loop { - use tokio::io::AsyncWriteExt; - match receiver.recv().await { - Some(mut bytes) => { - while !bytes.is_empty() { - match writer.write_buf(&mut bytes).await { - Ok(0) => { - let _ = result_sender.send(Ok(StreamState::Closed)).await; - break 'outer; - } - Ok(_) => { - if bytes.is_empty() { - match result_sender.send(Ok(StreamState::Open)).await { - Ok(_) => break, - Err(_) => break 'outer, + crate::preview2::in_tokio(|| { + tokio::spawn(async move { + 'outer: loop { + use tokio::io::AsyncWriteExt; + match receiver.recv().await { + Some(mut bytes) => { + while !bytes.is_empty() { + match writer.write_buf(&mut bytes).await { + Ok(0) => { + let _ = result_sender.send(Ok(StreamState::Closed)).await; + break 'outer; + } + Ok(_) => { + if bytes.is_empty() { + match result_sender.send(Ok(StreamState::Open)).await { + Ok(_) => break, + Err(_) => break 'outer, + } } + continue; + } + Err(e) => { + let _ = result_sender.send(Err(e)).await; + break 'outer; } - continue; - } - Err(e) => { - let _ = result_sender.send(Err(e)).await; - break 'outer; } } } - } - // The other side of the channel hung up, the task can exit now - None => break 'outer, + // The other side of the channel hung up, the task can exit now + None => break 'outer, + } } - } + }) }); AsyncWriteStream { From db176246042addddbb2c8c18894a0dc39b9ea191 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 14 Jul 2023 17:07:08 -0700 Subject: [PATCH 074/118] delete AsyncFdStream for now it turns out to be kinda hard and we can always work on adding it back in later. --- crates/wasi/src/preview2/stream.rs | 107 ----------------------------- 1 file changed, 107 deletions(-) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index c2771ea858c2..b46e73f5b168 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -392,113 +392,6 @@ impl HostOutputStream for AsyncWriteStream { } } -#[cfg(unix)] -pub use async_fd_stream::*; - -#[cfg(unix)] -mod async_fd_stream { - use super::{HostInputStream, HostOutputStream, StreamState}; - use anyhow::Error; - use bytes::Bytes; - use std::io::{Read, Write}; - use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; - use tokio::io::unix::AsyncFd; - - /// Provides a [`HostInputStream`] and [`HostOutputStream`] impl from an - /// [`std::os::fd::AsRawFd`] impl, using [`tokio::io::unix::AsyncFd`] - pub struct AsyncFdStream { - fd: AsyncFd, - } - - impl AsyncFdStream { - /// Create a [`AsyncFdStream`] from a type which implements [`AsRawFd`]. - /// This constructor will use `fcntl(2)` to set the `O_NONBLOCK` flag - /// if it is not already set. - /// The implementation of this constructor creates an - /// [`tokio::io::unix::AsyncFd`]. It will return an error unless - /// called from inside a tokio context. Additionally, tokio (via mio) - /// will register the fd inside with `epoll(7)` (or equiv on - /// macos). The process may only make one registration of an fd at a - /// time. If another registration exists, this constructor will return - /// an error. - pub fn new(fd: T) -> anyhow::Result { - use rustix::fs::OFlags; - let borrowed_fd = unsafe { rustix::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) }; - tokio::task::block_in_place(|| { - let flags = rustix::fs::fcntl_getfl(borrowed_fd)?; - if !flags.contains(OFlags::NONBLOCK) { - rustix::fs::fcntl_setfl(borrowed_fd, flags.difference(OFlags::NONBLOCK))?; - } - Ok::<(), anyhow::Error>(()) - })?; - Ok(Self { - fd: AsyncFd::new(fd)?, - }) - } - } - - #[async_trait::async_trait] - impl HostInputStream for AsyncFdStream { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // // we're done. - // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - // - // // Ensured this is nonblocking at construction of AsyncFdStream. - // let read_res = file.read(dest); - // - // // Make sure that the file doesn't close the fd when it's dropped. - // file.into_raw_fd(); - // - // let n = read_res?; - // - // // TODO: figure out when the stream should be considered closed - // // TODO: figure out how to handle the error conditions from the read call above - // - // Ok((n as u64, StreamState::Open)) - todo!() - } - - async fn ready(&mut self) -> Result<(), Error> { - /* - let _ = self.fd.readable().await?; - Ok(()) - */ - todo!() - } - } - - #[async_trait::async_trait] - impl HostOutputStream for AsyncFdStream { - fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error> { - // // Safety: we're the only one accessing this fd, and we turn it back into a raw fd when - // // we're done. - // let mut file = unsafe { std::fs::File::from_raw_fd(self.fd.as_raw_fd()) }; - // - // // Ensured this is nonblocking at construction of AsyncFdStream. - // let write_res = file.write(buf); - // - // // Make sure that the file doesn't close the fd when it's dropped. - // file.into_raw_fd(); - // - // let n = write_res?; - // - // // TODO: figure out how to handle the error conditions from the write call above - // - // Ok(n as u64) - todo!() - } - - async fn ready(&mut self) -> Result<(), Error> { - /* - let _ = self.fd.writable().await?; - Ok(()) - */ - todo!() - } - } -} - #[cfg(test)] mod test { use super::*; From 146a58ba90ac451a827b248e069b3d63a8a41011 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 14:08:42 -0700 Subject: [PATCH 075/118] Implement some memory pipe operations, and move async wrappers to the pipe mod --- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/pipe.rs | 876 ++++++++++++++++++++----- crates/wasi/src/preview2/stdio.rs | 2 +- crates/wasi/src/preview2/stdio/unix.rs | 2 +- crates/wasi/src/preview2/stream.rs | 687 +------------------ 5 files changed, 714 insertions(+), 855 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 1f657c1f2104..3444839a7138 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -37,7 +37,7 @@ pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; pub use self::random::{thread_rng, Deterministic}; pub use self::stream::{ - AsyncReadStream, AsyncWriteStream, HostInputStream, HostOutputStream, StreamState, + HostInputStream, HostOutputStream, StreamState, TableStreamExt, }; pub use self::table::{Table, TableError}; diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index c2f1b78e4339..f6ae8e21a59f 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -11,236 +11,780 @@ use crate::preview2::{HostInputStream, HostOutputStream, StreamState}; use anyhow::Error; use bytes::Bytes; -pub fn pipe(bound: usize) -> (InputPipe, OutputPipe) { - let (writer, reader) = tokio::sync::mpsc::channel(bound); +#[derive(Debug)] +pub struct MemoryInputPipe { + buffer: std::io::Cursor, +} - (InputPipe::new(reader), OutputPipe::new(writer)) +impl MemoryInputPipe { + pub fn new(bytes: Bytes) -> Self { + Self { + buffer: std::io::Cursor::new(bytes), + } + } + + pub fn is_empty(&self) -> bool { + self.buffer.get_ref().len() as u64 == self.buffer.position() + } } -pub struct InputPipe { - state: StreamState, - buffer: Vec, - channel: tokio::sync::mpsc::Receiver>, +#[async_trait::async_trait] +impl HostInputStream for MemoryInputPipe { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { + if self.is_empty() { + return Ok((Bytes::new(), StreamState::Closed)); + } + + let mut dest = bytes::BytesMut::zeroed(size); + let nbytes = std::io::Read::read(&mut self.buffer, dest.as_mut())?; + dest.truncate(nbytes); + + let state = if self.is_empty() { + StreamState::Closed + } else { + StreamState::Open + }; + Ok((dest.freeze(), state)) + } + async fn ready(&mut self) -> Result<(), Error> { + if !self.is_empty() { + Ok(()) + } else { + futures::future::pending().await + } + } } -impl InputPipe { - fn new(channel: tokio::sync::mpsc::Receiver>) -> Self { - Self { - state: StreamState::Open, - buffer: Vec::new(), - channel, +#[derive(Debug, Clone)] +pub struct MemoryOutputPipe { + buffer: std::sync::Arc>, +} + +impl MemoryOutputPipe { + pub fn new() -> Self { + MemoryOutputPipe { + buffer: std::sync::Arc::new(std::sync::Mutex::new(bytes::BytesMut::new())), } } + + pub fn contents(&self) -> bytes::Bytes { + self.buffer.lock().unwrap().clone().freeze() + } + + pub fn try_into_inner(self) -> Option { + std::sync::Arc::into_inner(self.buffer).map(|m| m.into_inner().unwrap()) + } } #[async_trait::async_trait] -impl HostInputStream for InputPipe { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // use tokio::sync::mpsc::error::TryRecvError; - // let read_from_buffer = self.buffer.len().min(dest.len()); - // let buffer_dest = &mut dest[..read_from_buffer]; - // buffer_dest.copy_from_slice(&self.buffer[..read_from_buffer]); - // // Keep remaining contents in buffer - // self.buffer = self.buffer.split_off(read_from_buffer); - // if read_from_buffer < dest.len() { - // match self.channel.try_recv() { - // Ok(msg) => { - // let recv_dest = &mut dest[read_from_buffer..]; - // if msg.len() < recv_dest.len() { - // recv_dest[..msg.len()].copy_from_slice(&msg); - // Ok(((read_from_buffer + msg.len()) as u64, self.state)) - // } else { - // recv_dest.copy_from_slice(&msg[..recv_dest.len()]); - // self.buffer.extend_from_slice(&msg[recv_dest.len()..]); - // Ok((dest.len() as u64, self.state)) - // } - // } - // Err(TryRecvError::Empty) => Ok((read_from_buffer as u64, self.state)), - // Err(TryRecvError::Disconnected) => { - // self.state = StreamState::Closed; - // Ok((read_from_buffer as u64, self.state)) - // } - // } - // } else { - // Ok((read_from_buffer as u64, self.state)) - // } - todo!() +impl HostOutputStream for MemoryOutputPipe { + fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { + let mut buf = self.buffer.lock().unwrap(); + buf.extend_from_slice(bytes.as_ref()); + Ok((bytes.len(), StreamState::Open)) } async fn ready(&mut self) -> Result<(), Error> { - match self.channel.recv().await { - None => self.state = StreamState::Closed, - Some(mut buf) => self.buffer.append(&mut buf), - } + // This stream is always ready for writing. Ok(()) } } -enum SenderState { - Writable(tokio::sync::mpsc::OwnedPermit>), - Channel(tokio::sync::mpsc::Sender>), +/// TODO +pub fn pipe(size: usize) -> (AsyncReadStream, AsyncWriteStream) { + let (a, b) = tokio::io::duplex(size); + let (_read_half, write_half) = tokio::io::split(a); + let (read_half, _write_half) = tokio::io::split(b); + ( + AsyncReadStream::new(read_half), + AsyncWriteStream::new(write_half), + ) } -pub struct OutputPipe { - buffer: Vec, - channel: Option, +/// Provides a [`HostInputStream`] impl from a [`tokio::io::AsyncRead`] impl +pub struct AsyncReadStream { + state: StreamState, + buffer: Option>, + receiver: tokio::sync::mpsc::Receiver>, } -impl OutputPipe { - fn new(s: tokio::sync::mpsc::Sender>) -> Self { - Self { - buffer: Vec::new(), - channel: Some(SenderState::Channel(s)), +impl AsyncReadStream { + /// Create a [`AsyncReadStream`]. In order to use the [`HostInputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. + pub fn new(mut reader: T) -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(1); + crate::preview2::in_tokio(|| { + tokio::spawn(async move { + loop { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = match reader.read_buf(&mut buf).await { + Ok(nbytes) if nbytes == 0 => { + sender.send(Ok((Bytes::new(), StreamState::Closed))).await + } + Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, + Err(e) => sender.send(Err(e)).await, + }; + if sent.is_err() { + // no more receiver - stop trying to read + break; + } + } + }) + }); + AsyncReadStream { + state: StreamState::Open, + buffer: None, + receiver, } } +} - async fn blocking_send(&mut self, buf: Vec) -> Result<(), Error> { - let s = match self.take_channel() { - SenderState::Writable(p) => { - let s = p.send(buf); - SenderState::Channel(s) - } +#[async_trait::async_trait] +impl HostInputStream for AsyncReadStream { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { + use tokio::sync::mpsc::error::TryRecvError; - SenderState::Channel(s) => { - s.send(buf).await?; - SenderState::Channel(s) + match self.buffer.take() { + Some(Ok(mut bytes)) => { + // TODO: de-duplicate the buffer management with the case below + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + let return_state = if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + StreamState::Open + } else { + self.state + }; + return Ok((bytes, return_state)); } - }; + Some(Err(e)) => return Err(e.into()), + None => {} + } - self.channel = Some(s); + match self.receiver.try_recv() { + Ok(Ok((mut bytes, state))) => { + self.state = state; - Ok(()) + let len = bytes.len().min(size); + let rest = bytes.split_off(len); + let return_state = if !rest.is_empty() { + self.buffer = Some(Ok(rest)); + StreamState::Open + } else { + self.state + }; + + Ok((bytes, return_state)) + } + Ok(Err(e)) => Err(e.into()), + Err(TryRecvError::Empty) => Ok((Bytes::new(), self.state)), + Err(TryRecvError::Disconnected) => Err(anyhow::anyhow!( + "AsyncReadStream sender died - should be impossible" + )), + } } - async fn flush(&mut self) { - if self.buffer.is_empty() { - return; + async fn ready(&mut self) -> Result<(), Error> { + if self.buffer.is_some() || self.state == StreamState::Closed { + return Ok(()); } + match self.receiver.recv().await { + Some(Ok((bytes, state))) => { + if state == StreamState::Closed { + self.state = state; + } + self.buffer = Some(Ok(bytes)); + } + Some(Err(e)) => self.buffer = Some(Err(e)), + None => { + return Err(anyhow::anyhow!( + "no more sender for an open AsyncReadStream - should be impossible" + )) + } + } + Ok(()) + } +} - let bytes = core::mem::take(&mut self.buffer); +#[derive(Debug)] +enum WriteState { + Ready, + Pending, + Err(std::io::Error), +} - self.blocking_send(bytes) - .await - .expect("fixme: handle closed write end later") +/// Provides a [`HostOutputStream`] impl from a [`tokio::io::AsyncWrite`] impl +pub struct AsyncWriteStream { + state: Option, + sender: tokio::sync::mpsc::Sender, + result_receiver: tokio::sync::mpsc::Receiver>, +} + +impl AsyncWriteStream { + /// Create a [`AsyncWriteStream`]. In order to use the [`HostOutputStream`] impl + /// provided by this struct, the argument must impl [`tokio::io::AsyncWrite`]. + pub fn new(mut writer: T) -> Self { + let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); + let (result_sender, result_receiver) = tokio::sync::mpsc::channel(1); + + crate::preview2::in_tokio(|| { + tokio::spawn(async move { + 'outer: loop { + use tokio::io::AsyncWriteExt; + match receiver.recv().await { + Some(mut bytes) => { + println!("got bytes: {:?}", bytes); + while !bytes.is_empty() { + match writer.write_buf(&mut bytes).await { + Ok(0) => { + let _ = result_sender.send(Ok(StreamState::Closed)).await; + break 'outer; + } + Ok(_) => { + if bytes.is_empty() { + match result_sender.send(Ok(StreamState::Open)).await { + Ok(_) => break, + Err(_) => break 'outer, + } + } + continue; + } + Err(e) => { + let _ = result_sender.send(Err(e)).await; + break 'outer; + } + } + } + } + + // The other side of the channel hung up, the task can exit now + None => break 'outer, + } + } + }) + }); + + AsyncWriteStream { + state: Some(WriteState::Ready), + sender, + result_receiver, + } } - fn take_channel(&mut self) -> SenderState { - self.channel.take().expect("Missing channel state") + fn send(&mut self, bytes: Bytes) -> anyhow::Result<(usize, StreamState)> { + use tokio::sync::mpsc::error::TrySendError; + + debug_assert!(matches!(self.state, Some(WriteState::Ready))); + + let len = bytes.len(); + match self.sender.try_send(bytes) { + Ok(_) => { + self.state = Some(WriteState::Pending); + Ok((len, StreamState::Open)) + } + Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), + Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), + } } } #[async_trait::async_trait] -impl HostOutputStream for OutputPipe { - fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { - // use tokio::sync::mpsc::error::TrySendError; - // - // let mut bytes = core::mem::take(&mut self.buffer); - // bytes.extend(buf); - // let (s, bytes) = match self.take_channel() { - // SenderState::Writable(p) => { - // let s = p.send(bytes); - // (s, Vec::new()) - // } - // - // SenderState::Channel(s) => match s.try_send(bytes) { - // Ok(()) => (s, Vec::new()), - // Err(TrySendError::Full(b)) => (s, b), - // Err(TrySendError::Closed(_)) => { - // // TODO: we may need to communicate failure out in a way that doesn't result in - // // a trap. - // return Err(anyhow::anyhow!("pipe closed")); - // } - // }, - // }; - // - // self.buffer = bytes; - // self.channel = Some(SenderState::Channel(s)); - // - // Ok(buf.len() as u64) - todo!() +impl HostOutputStream for AsyncWriteStream { + fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { + use tokio::sync::mpsc::error::TryRecvError; + + dbg!(&self.state); + match self.state { + Some(WriteState::Ready) => self.send(bytes), + Some(WriteState::Pending) => match self.result_receiver.try_recv() { + Ok(Ok(StreamState::Open)) => { + self.state = Some(WriteState::Ready); + self.send(bytes) + } + + Ok(Ok(StreamState::Closed)) => { + self.state = None; + Ok((0, StreamState::Closed)) + } + + Ok(Err(e)) => { + self.state = None; + Err(e.into()) + } + + Err(TryRecvError::Empty) => { + self.state = Some(WriteState::Pending); + Ok((0, StreamState::Open)) + } + + Err(TryRecvError::Disconnected) => { + unreachable!("task shouldn't die while pending") + } + }, + Some(WriteState::Err(_)) => { + // Move the error payload out of self.state, because errors are not Copy, + // and set self.state to None, because the stream is now closed. + if let Some(WriteState::Err(e)) = self.state.take() { + Err(e.into()) + } else { + unreachable!("self.state shown to be Some(Err(e)) in match clause") + } + } + + None => Ok((0, StreamState::Closed)), + } } async fn ready(&mut self) -> Result<(), Error> { - self.flush().await; - let p = match self.channel.take().expect("Missing sender channel state") { - SenderState::Writable(p) => p, - SenderState::Channel(s) => s.reserve_owned().await?, - }; + match &self.state { + Some(WriteState::Pending) => match self.result_receiver.recv().await { + Some(Ok(StreamState::Open)) => { + self.state = Some(WriteState::Ready); + } + + Some(Ok(StreamState::Closed)) => { + self.state = None; + } + + Some(Err(e)) => { + self.state = Some(WriteState::Err(e)); + } - self.channel = Some(SenderState::Writable(p)); + None => unreachable!("task shouldn't die while pending"), + }, + + Some(WriteState::Ready | WriteState::Err(_)) | None => {} + } Ok(()) } } -#[derive(Debug)] -pub struct MemoryInputPipe { - buffer: std::io::Cursor>, -} +#[cfg(test)] +mod test { + use super::*; + use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -impl MemoryInputPipe { - pub fn new(bytes: impl AsRef<[u8]>) -> Self { - Self { - buffer: std::io::Cursor::new(Vec::from(bytes.as_ref())), + pub fn simplex(size: usize) -> (impl AsyncRead, impl AsyncWrite) { + let (a, b) = tokio::io::duplex(size); + let (_read_half, write_half) = tokio::io::split(a); + let (read_half, _write_half) = tokio::io::split(b); + (read_half, write_half) + } + + #[tokio::test(flavor = "multi_thread")] + async fn empty_read_stream() { + let mut reader = AsyncReadStream::new(tokio::io::empty()); + let (bs, state) = reader.read(10).unwrap(); + assert!(bs.is_empty()); + + // In a multi-threaded context, the value of state is not deterministic -- the spawned + // reader task may run on a different thread. + match state { + // The reader task ran before we tried to read, and noticed that the input was empty. + StreamState::Closed => {} + + // The reader task hasn't run yet. Call `ready` to await and fill the buffer. + StreamState::Open => { + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + let (bs, state) = reader.read(0).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Closed); + } } } -} -#[async_trait::async_trait] -impl HostInputStream for MemoryInputPipe { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // let nbytes = std::io::Read::read(&mut self.buffer, dest)?; - // let state = if self.buffer.get_ref().len() as u64 == self.buffer.position() { - // StreamState::Closed - // } else { - // StreamState::Open - // }; - // Ok((nbytes as u64, state)) - todo!() + #[tokio::test(flavor = "multi_thread")] + async fn infinite_read_stream() { + let mut reader = AsyncReadStream::new(tokio::io::repeat(0)); + + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(bs.len(), 10); + assert_eq!(state, StreamState::Open); + } else { + assert_eq!(bs.len(), 10); + } + + // Subsequent reads should succeed + let (bs, state) = reader.read(10).unwrap(); + assert_eq!(state, StreamState::Open); + assert_eq!(bs.len(), 10); + + // Even 0-length reads should succeed and show its open + let (bs, state) = reader.read(0).unwrap(); + assert_eq!(state, StreamState::Open); + assert_eq!(bs.len(), 0); } - async fn ready(&mut self) -> Result<(), Error> { - if self.buffer.get_ref().len() as u64 > self.buffer.position() { - Ok(()) + + async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + Sync + 'static { + let (r, mut w) = simplex(contents.len()); + w.write_all(contents).await.unwrap(); + r + } + + #[tokio::test(flavor = "multi_thread")] + async fn finite_read_stream() { + let mut reader = AsyncReadStream::new(finite_async_reader(&[1; 123]).await); + + let (bs, state) = reader.read(123).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(123).unwrap(); + assert_eq!(bs.len(), 123); + assert_eq!(state, StreamState::Open); } else { - // FIXME - //futures_util::future::pending().await - todo!() + assert_eq!(bs.len(), 123); + } + + // The AsyncRead's should be empty now, but we have a race where the reader task hasn't + // yet send that to the AsyncReadStream. + let (bs, state) = reader.read(0).unwrap(); + assert!(bs.is_empty()); + match state { + StreamState::Closed => {} // Correct! + StreamState::Open => { + // Need to await to give this side time to catch up + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should show closed + let (bs, state) = reader.read(0).unwrap(); + assert_eq!(bs.len(), 0); + assert_eq!(state, StreamState::Closed); + } } } -} -#[derive(Debug, Clone)] -pub struct MemoryOutputPipe { - buffer: std::sync::Arc>>, -} + #[tokio::test(flavor = "multi_thread")] + // Test that you can write items into the stream, and they get read out in the order they were + // written, with the proper indications of readiness for reading: + async fn multiple_chunks_read_stream() { + let (r, mut w) = simplex(1024); + let mut reader = AsyncReadStream::new(r); -impl MemoryOutputPipe { - pub fn new() -> Self { - MemoryOutputPipe { - buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + w.write_all(&[123]).await.unwrap(); + + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(state, StreamState::Open); + if bs.is_empty() { + // Reader task hasn't run yet. Call `ready` to await and fill the buffer. + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + // Now a read should succeed + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(*bs, [123u8]); + assert_eq!(state, StreamState::Open); + } else { + assert_eq!(*bs, [123u8]); } + + // The stream should be empty and open now: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // We can wait on readiness and it will time out: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .err() + .expect("the reader should time out"); + + // Still open and empty: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // Put something else in the stream: + w.write_all(&[45]).await.unwrap(); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("the ready is ok"); + + // read the something else back out: + let (bs, state) = reader.read(1).unwrap(); + assert_eq!(*bs, [45u8]); + assert_eq!(state, StreamState::Open); + + // nothing else in there: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // We can wait on readiness and it will time out: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .err() + .expect("the reader should time out"); + + // nothing else in there: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Open); + + // Now close the pipe: + drop(w); + + // Wait readiness (yes we could possibly win the race and read it out faster, leaving that + // out of the test for simplicity) + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("the ready is ok"); + + // empty and now closed: + let (bs, state) = reader.read(1).unwrap(); + assert!(bs.is_empty()); + assert_eq!(state, StreamState::Closed); + } + + #[tokio::test(flavor = "multi_thread")] + // At the moment we are restricting AsyncReadStream from buffering more than 4k. This isn't a + // suitable design for all applications, and we will probably make a knob or change the + // behavior at some point, but this test shows the behavior as it is implemented: + async fn backpressure_read_stream() { + let (r, mut w) = simplex(16 * 1024); // Make sure this buffer isnt a bottleneck + let mut reader = AsyncReadStream::new(r); + + let writer_task = tokio::task::spawn(async move { + // Write twice as much as we can buffer up in an AsyncReadStream: + w.write_all(&[123; 8192]).await.unwrap(); + w + }); + + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Now we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + assert_eq!(state, StreamState::Open); + + // Allow the crank to turn more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Again we expect the reader task has sent 4k from the stream to the reader. + // Try to read out one bigger than the buffer available: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 4096); + assert_eq!(state, StreamState::Open); + + // The writer task is now finished - join with it: + let w = tokio::time::timeout(std::time::Duration::from_millis(10), writer_task) + .await + .expect("the join should be ready instantly"); + // And close the pipe: + drop(w); + + // Allow the crank to turn more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + .await + .expect("the reader should be ready instantly") + .expect("ready is ok"); + + // Now we expect the reader to be empty, and the stream closed: + let (bs, state) = reader.read(4097).unwrap(); + assert_eq!(bs.len(), 0); + assert_eq!(state, StreamState::Closed); } - pub fn contents(&self) -> Vec { - self.buffer.lock().unwrap().clone() + + #[tokio::test(flavor = "multi_thread")] + async fn sink_write_stream() { + let mut writer = AsyncWriteStream::new(tokio::io::sink()); + let chunk = Bytes::from_static(&[0; 1024]); + + // I can write whatever: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // But I expect this to block additional writes: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // Now additional writes will work: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); } - pub fn try_into_inner(self) -> Result, Self> { - match std::sync::Arc::try_unwrap(self.buffer) { - Ok(m) => Ok(m.into_inner().map_err(|_| ()).expect("mutex poisioned")), - Err(buffer) => Err(Self { buffer }), - } + + #[tokio::test(flavor = "multi_thread")] + async fn closed_write_stream() { + let (reader, writer) = simplex(1024); + drop(reader); + let mut writer = AsyncWriteStream::new(writer); + + // Without checking write readiness, perform a nonblocking write: this should succeed + // because we will buffer up the write. + let chunk = Bytes::from_static(&[0; 1]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // Check write readiness: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // When we drop the simplex reader, that causes the simplex writer to return BrokenPipe on + // its write. Now that the buffering crank has turned, our next write will give BrokenPipe. + let err = writer.write(chunk.clone()).err().unwrap(); + assert_eq!( + err.downcast_ref::().unwrap().kind(), + std::io::ErrorKind::BrokenPipe + ); + + // Now that we got the error out of the writer, it should be closed - subsequent writes + // will not work + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Closed); } -} -#[async_trait::async_trait] -impl HostOutputStream for MemoryOutputPipe { - fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), anyhow::Error> { - // self.buffer.lock().unwrap().extend(buf); - // Ok(buf.len() as u64) - todo!() + #[tokio::test(flavor = "multi_thread")] + async fn multiple_chunks_write_stream() { + use std::ops::Deref; + + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(writer); + + // Write a chunk: + let chunk = Bytes::from_static(&[123; 1]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // After the write, still ready for more writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + let mut read_buf = vec![0; chunk.len()]; + let read_len = reader.read_exact(&mut read_buf).await.unwrap(); + assert_eq!(read_len, chunk.len()); + assert_eq!(read_buf.as_slice(), chunk.deref()); + + // Write a second, different chunk: + let chunk2 = Bytes::from_static(&[45; 1]); + let (len, state) = writer.write(chunk2.clone()).unwrap(); + assert_eq!(len, chunk2.len()); + assert_eq!(state, StreamState::Open); + + // After the write, still ready for more writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + let mut read2_buf = vec![0; chunk2.len()]; + let read2_len = reader.read_exact(&mut read2_buf).await.unwrap(); + assert_eq!(read2_len, chunk2.len()); + assert_eq!(read2_buf.as_slice(), chunk2.deref()); } - async fn ready(&mut self) -> Result<(), Error> { - // This stream is always ready for writing. - Ok(()) + #[tokio::test(flavor = "multi_thread")] + async fn backpressure_write_stream() { + // Stream can buffer up to 1k, plus one write chunk, before not + // accepting more input: + let (mut reader, writer) = simplex(1024); + let mut writer = AsyncWriteStream::new(writer); + + // Write enough to fill the simplex buffer: + let chunk = Bytes::from_static(&[0; 1024]); + let (len, state) = writer.write(chunk.clone()).unwrap(); + + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // turn the crank and it should be ready for writing again: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // Now fill the buffer between here and the writer task: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); + + // Try shoving even more down there, and it shouldnt accept more input: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + // turn the crank and it should Not become ready for writing until we read something out. + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .err() + .expect("the writer should be not become ready"); + + // Still not ready from the .write interface either: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, 0); + assert_eq!(state, StreamState::Open); + + // There is 2k in the buffer. I should be able to read all of it out: + let mut buf = [0; 2048]; + reader.read_exact(&mut buf).await.unwrap(); + + // and no more: + tokio::time::timeout(std::time::Duration::from_millis(10), reader.read(&mut buf)) + .await + .err() + .expect("nothing more buffered in the system"); + + // Now the backpressure should be cleared, and an additional write should be accepted. + + // immediately ready for writing: + tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + .await + .expect("the writer should be ready instantly") + .expect("ready is ok"); + + // and the write succeeds: + let (len, state) = writer.write(chunk.clone()).unwrap(); + assert_eq!(len, chunk.len()); + assert_eq!(state, StreamState::Open); } } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 9ed36491cbf6..154730545912 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,7 +1,7 @@ use anyhow::Error; use bytes::Bytes; -use crate::preview2::{AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; +use crate::preview2::{pipe::AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; #[cfg(unix)] mod unix; diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index 412edd220f16..ac0bb2f33da8 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -1,4 +1,4 @@ -use crate::preview2::{AsyncReadStream, StreamState}; +use crate::preview2::{pipe::AsyncReadStream, StreamState}; use anyhow::Error; use bytes::Bytes; use tokio::io::unix::AsyncFd; diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index b46e73f5b168..d4b7ea494580 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -133,269 +133,10 @@ impl TableStreamExt for Table { } } -/// Provides a [`HostInputStream`] impl from a [`tokio::io::AsyncRead`] impl -pub struct AsyncReadStream { - state: StreamState, - buffer: Option>, - receiver: tokio::sync::mpsc::Receiver>, -} - -impl AsyncReadStream { - /// Create a [`AsyncReadStream`]. In order to use the [`HostInputStream`] impl - /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. - pub fn new(mut reader: T) -> Self { - let (sender, receiver) = tokio::sync::mpsc::channel(1); - crate::preview2::in_tokio(|| { - tokio::spawn(async move { - loop { - use tokio::io::AsyncReadExt; - let mut buf = bytes::BytesMut::with_capacity(4096); - let sent = match reader.read_buf(&mut buf).await { - Ok(nbytes) if nbytes == 0 => { - sender.send(Ok((Bytes::new(), StreamState::Closed))).await - } - Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, - Err(e) => sender.send(Err(e)).await, - }; - if sent.is_err() { - // no more receiver - stop trying to read - break; - } - } - }) - }); - AsyncReadStream { - state: StreamState::Open, - buffer: None, - receiver, - } - } -} - -#[async_trait::async_trait] -impl HostInputStream for AsyncReadStream { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - use tokio::sync::mpsc::error::TryRecvError; - - match self.buffer.take() { - Some(Ok(mut bytes)) => { - // TODO: de-duplicate the buffer management with the case below - let len = bytes.len().min(size); - let rest = bytes.split_off(len); - let return_state = if !rest.is_empty() { - self.buffer = Some(Ok(rest)); - StreamState::Open - } else { - self.state - }; - return Ok((bytes, return_state)); - } - Some(Err(e)) => return Err(e.into()), - None => {} - } - - match self.receiver.try_recv() { - Ok(Ok((mut bytes, state))) => { - self.state = state; - - let len = bytes.len().min(size); - let rest = bytes.split_off(len); - let return_state = if !rest.is_empty() { - self.buffer = Some(Ok(rest)); - StreamState::Open - } else { - self.state - }; - - Ok((bytes, return_state)) - } - Ok(Err(e)) => Err(e.into()), - Err(TryRecvError::Empty) => Ok((Bytes::new(), self.state)), - Err(TryRecvError::Disconnected) => Err(anyhow::anyhow!( - "AsyncReadStream sender died - should be impossible" - )), - } - } - - async fn ready(&mut self) -> Result<(), Error> { - if self.buffer.is_some() || self.state == StreamState::Closed { - return Ok(()); - } - match self.receiver.recv().await { - Some(Ok((bytes, state))) => { - if state == StreamState::Closed { - self.state = state; - } - self.buffer = Some(Ok(bytes)); - } - Some(Err(e)) => self.buffer = Some(Err(e)), - None => { - return Err(anyhow::anyhow!( - "no more sender for an open AsyncReadStream - should be impossible" - )) - } - } - Ok(()) - } -} - -#[derive(Debug)] -enum WriteState { - Ready, - Pending, - Err(std::io::Error), -} - -/// Provides a [`HostOutputStream`] impl from a [`tokio::io::AsyncWrite`] impl -pub struct AsyncWriteStream { - state: Option, - sender: tokio::sync::mpsc::Sender, - result_receiver: tokio::sync::mpsc::Receiver>, -} - -impl AsyncWriteStream { - /// Create a [`AsyncWriteStream`]. In order to use the [`HostOutputStream`] impl - /// provided by this struct, the argument must impl [`tokio::io::AsyncWrite`]. - pub fn new(mut writer: T) -> Self { - let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); - let (result_sender, result_receiver) = tokio::sync::mpsc::channel(1); - - crate::preview2::in_tokio(|| { - tokio::spawn(async move { - 'outer: loop { - use tokio::io::AsyncWriteExt; - match receiver.recv().await { - Some(mut bytes) => { - while !bytes.is_empty() { - match writer.write_buf(&mut bytes).await { - Ok(0) => { - let _ = result_sender.send(Ok(StreamState::Closed)).await; - break 'outer; - } - Ok(_) => { - if bytes.is_empty() { - match result_sender.send(Ok(StreamState::Open)).await { - Ok(_) => break, - Err(_) => break 'outer, - } - } - continue; - } - Err(e) => { - let _ = result_sender.send(Err(e)).await; - break 'outer; - } - } - } - } - - // The other side of the channel hung up, the task can exit now - None => break 'outer, - } - } - }) - }); - - AsyncWriteStream { - state: Some(WriteState::Ready), - sender, - result_receiver, - } - } - - fn send(&mut self, bytes: Bytes) -> anyhow::Result<(usize, StreamState)> { - use tokio::sync::mpsc::error::TrySendError; - - debug_assert!(matches!(self.state, Some(WriteState::Ready))); - - let len = bytes.len(); - match self.sender.try_send(bytes) { - Ok(_) => { - self.state = Some(WriteState::Pending); - Ok((len, StreamState::Open)) - } - Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), - Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), - } - } -} - -#[async_trait::async_trait] -impl HostOutputStream for AsyncWriteStream { - fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { - use tokio::sync::mpsc::error::TryRecvError; - - dbg!(&self.state); - match self.state { - Some(WriteState::Ready) => self.send(bytes), - Some(WriteState::Pending) => match self.result_receiver.try_recv() { - Ok(Ok(StreamState::Open)) => { - self.state = Some(WriteState::Ready); - self.send(bytes) - } - - Ok(Ok(StreamState::Closed)) => { - self.state = None; - Ok((0, StreamState::Closed)) - } - - Ok(Err(e)) => { - self.state = None; - Err(e.into()) - } - - Err(TryRecvError::Empty) => { - self.state = Some(WriteState::Pending); - Ok((0, StreamState::Open)) - } - - Err(TryRecvError::Disconnected) => { - unreachable!("task shouldn't die while pending") - } - }, - Some(WriteState::Err(_)) => { - // Move the error payload out of self.state, because errors are not Copy, - // and set self.state to None, because the stream is now closed. - if let Some(WriteState::Err(e)) = self.state.take() { - Err(e.into()) - } else { - unreachable!("self.state shown to be Some(Err(e)) in match clause") - } - } - - None => Ok((0, StreamState::Closed)), - } - } - - async fn ready(&mut self) -> Result<(), Error> { - match &self.state { - Some(WriteState::Pending) => match self.result_receiver.recv().await { - Some(Ok(StreamState::Open)) => { - self.state = Some(WriteState::Ready); - } - - Some(Ok(StreamState::Closed)) => { - self.state = None; - } - - Some(Err(e)) => { - self.state = Some(WriteState::Err(e)); - } - - None => unreachable!("task shouldn't die while pending"), - }, - - Some(WriteState::Ready | WriteState::Err(_)) | None => {} - } - - Ok(()) - } -} - #[cfg(test)] mod test { use super::*; - use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + #[test] fn input_stream_in_table() { struct DummyInputStream; @@ -438,430 +179,4 @@ mod test { let _ = table.get_output_stream_mut(ix).unwrap(); } - #[tokio::test(flavor = "multi_thread")] - async fn empty_read_stream() { - let mut reader = AsyncReadStream::new(tokio::io::empty()); - let (bs, state) = reader.read(10).unwrap(); - assert!(bs.is_empty()); - - // In a multi-threaded context, the value of state is not deterministic -- the spawned - // reader task may run on a different thread. - match state { - // The reader task ran before we tried to read, and noticed that the input was empty. - StreamState::Closed => {} - - // The reader task hasn't run yet. Call `ready` to await and fill the buffer. - StreamState::Open => { - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - let (bs, state) = reader.read(0).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Closed); - } - } - } - - #[tokio::test(flavor = "multi_thread")] - async fn infinite_read_stream() { - let mut reader = AsyncReadStream::new(tokio::io::repeat(0)); - - let (bs, state) = reader.read(10).unwrap(); - assert_eq!(state, StreamState::Open); - if bs.is_empty() { - // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - // Now a read should succeed - let (bs, state) = reader.read(10).unwrap(); - assert_eq!(bs.len(), 10); - assert_eq!(state, StreamState::Open); - } else { - assert_eq!(bs.len(), 10); - } - - // Subsequent reads should succeed - let (bs, state) = reader.read(10).unwrap(); - assert_eq!(state, StreamState::Open); - assert_eq!(bs.len(), 10); - - // Even 0-length reads should succeed and show its open - let (bs, state) = reader.read(0).unwrap(); - assert_eq!(state, StreamState::Open); - assert_eq!(bs.len(), 0); - } - - fn simplex( - size: usize, - ) -> ( - impl AsyncRead + Send + Sync + 'static, - impl AsyncWrite + Send + Sync + 'static, - ) { - let (a, b) = tokio::io::duplex(size); - let (_read_half, write_half) = tokio::io::split(a); - let (read_half, _write_half) = tokio::io::split(b); - (read_half, write_half) - } - - async fn finite_async_reader(contents: &[u8]) -> impl AsyncRead + Send + Sync + 'static { - let (r, mut w) = simplex(contents.len()); - w.write_all(contents).await.unwrap(); - r - } - - #[tokio::test(flavor = "multi_thread")] - async fn finite_read_stream() { - let mut reader = AsyncReadStream::new(finite_async_reader(&[1; 123]).await); - - let (bs, state) = reader.read(123).unwrap(); - assert_eq!(state, StreamState::Open); - if bs.is_empty() { - // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - // Now a read should succeed - let (bs, state) = reader.read(123).unwrap(); - assert_eq!(bs.len(), 123); - assert_eq!(state, StreamState::Open); - } else { - assert_eq!(bs.len(), 123); - } - - // The AsyncRead's should be empty now, but we have a race where the reader task hasn't - // yet send that to the AsyncReadStream. - let (bs, state) = reader.read(0).unwrap(); - assert!(bs.is_empty()); - match state { - StreamState::Closed => {} // Correct! - StreamState::Open => { - // Need to await to give this side time to catch up - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - // Now a read should show closed - let (bs, state) = reader.read(0).unwrap(); - assert_eq!(bs.len(), 0); - assert_eq!(state, StreamState::Closed); - } - } - } - - #[tokio::test(flavor = "multi_thread")] - // Test that you can write items into the stream, and they get read out in the order they were - // written, with the proper indications of readiness for reading: - async fn multiple_chunks_read_stream() { - let (r, mut w) = simplex(1024); - let mut reader = AsyncReadStream::new(r); - - w.write_all(&[123]).await.unwrap(); - - let (bs, state) = reader.read(1).unwrap(); - assert_eq!(state, StreamState::Open); - if bs.is_empty() { - // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - // Now a read should succeed - let (bs, state) = reader.read(1).unwrap(); - assert_eq!(*bs, [123u8]); - assert_eq!(state, StreamState::Open); - } else { - assert_eq!(*bs, [123u8]); - } - - // The stream should be empty and open now: - let (bs, state) = reader.read(1).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Open); - - // We can wait on readiness and it will time out: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .err() - .expect("the reader should time out"); - - // Still open and empty: - let (bs, state) = reader.read(1).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Open); - - // Put something else in the stream: - w.write_all(&[45]).await.unwrap(); - - // Wait readiness (yes we could possibly win the race and read it out faster, leaving that - // out of the test for simplicity) - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("the ready is ok"); - - // read the something else back out: - let (bs, state) = reader.read(1).unwrap(); - assert_eq!(*bs, [45u8]); - assert_eq!(state, StreamState::Open); - - // nothing else in there: - let (bs, state) = reader.read(1).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Open); - - // We can wait on readiness and it will time out: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .err() - .expect("the reader should time out"); - - // nothing else in there: - let (bs, state) = reader.read(1).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Open); - - // Now close the pipe: - drop(w); - - // Wait readiness (yes we could possibly win the race and read it out faster, leaving that - // out of the test for simplicity) - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("the ready is ok"); - - // empty and now closed: - let (bs, state) = reader.read(1).unwrap(); - assert!(bs.is_empty()); - assert_eq!(state, StreamState::Closed); - } - - #[tokio::test(flavor = "multi_thread")] - // At the moment we are restricting AsyncReadStream from buffering more than 4k. This isn't a - // suitable design for all applications, and we will probably make a knob or change the - // behavior at some point, but this test shows the behavior as it is implemented: - async fn backpressure_read_stream() { - let (r, mut w) = simplex(16 * 1024); // Make sure this buffer isnt a bottleneck - let mut reader = AsyncReadStream::new(r); - - let writer_task = tokio::task::spawn(async move { - // Write twice as much as we can buffer up in an AsyncReadStream: - w.write_all(&[123; 8192]).await.unwrap(); - w - }); - - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - - // Now we expect the reader task has sent 4k from the stream to the reader. - // Try to read out one bigger than the buffer available: - let (bs, state) = reader.read(4097).unwrap(); - assert_eq!(bs.len(), 4096); - assert_eq!(state, StreamState::Open); - - // Allow the crank to turn more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - - // Again we expect the reader task has sent 4k from the stream to the reader. - // Try to read out one bigger than the buffer available: - let (bs, state) = reader.read(4097).unwrap(); - assert_eq!(bs.len(), 4096); - assert_eq!(state, StreamState::Open); - - // The writer task is now finished - join with it: - let w = tokio::time::timeout(std::time::Duration::from_millis(10), writer_task) - .await - .expect("the join should be ready instantly"); - // And close the pipe: - drop(w); - - // Allow the crank to turn more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) - .await - .expect("the reader should be ready instantly") - .expect("ready is ok"); - - // Now we expect the reader to be empty, and the stream closed: - let (bs, state) = reader.read(4097).unwrap(); - assert_eq!(bs.len(), 0); - assert_eq!(state, StreamState::Closed); - } - - #[tokio::test(flavor = "multi_thread")] - async fn sink_write_stream() { - let mut writer = AsyncWriteStream::new(tokio::io::sink()); - let chunk = Bytes::from_static(&[0; 1024]); - - // I can write whatever: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - - // But I expect this to block additional writes: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, 0); - assert_eq!(state, StreamState::Open); - - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - // Now additional writes will work: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - } - - #[tokio::test(flavor = "multi_thread")] - async fn closed_write_stream() { - let (reader, writer) = simplex(1024); - drop(reader); - let mut writer = AsyncWriteStream::new(writer); - - // Without checking write readiness, perform a nonblocking write: this should succeed - // because we will buffer up the write. - let chunk = Bytes::from_static(&[0; 1]); - let (len, state) = writer.write(chunk.clone()).unwrap(); - - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - - // Check write readiness: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - // When we drop the simplex reader, that causes the simplex writer to return BrokenPipe on - // its write. Now that the buffering crank has turned, our next write will give BrokenPipe. - let err = writer.write(chunk.clone()).err().unwrap(); - assert_eq!( - err.downcast_ref::().unwrap().kind(), - std::io::ErrorKind::BrokenPipe - ); - - // Now that we got the error out of the writer, it should be closed - subsequent writes - // will not work - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, 0); - assert_eq!(state, StreamState::Closed); - } - - #[tokio::test(flavor = "multi_thread")] - async fn multiple_chunks_write_stream() { - use std::ops::Deref; - - let (mut reader, writer) = simplex(1024); - let mut writer = AsyncWriteStream::new(writer); - - // Write a chunk: - let chunk = Bytes::from_static(&[123; 1]); - let (len, state) = writer.write(chunk.clone()).unwrap(); - - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - - // After the write, still ready for more writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - let mut read_buf = vec![0; chunk.len()]; - let read_len = reader.read_exact(&mut read_buf).await.unwrap(); - assert_eq!(read_len, chunk.len()); - assert_eq!(read_buf.as_slice(), chunk.deref()); - - // Write a second, different chunk: - let chunk2 = Bytes::from_static(&[45; 1]); - let (len, state) = writer.write(chunk2.clone()).unwrap(); - assert_eq!(len, chunk2.len()); - assert_eq!(state, StreamState::Open); - - // After the write, still ready for more writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - let mut read2_buf = vec![0; chunk2.len()]; - let read2_len = reader.read_exact(&mut read2_buf).await.unwrap(); - assert_eq!(read2_len, chunk2.len()); - assert_eq!(read2_buf.as_slice(), chunk2.deref()); - } - - #[tokio::test(flavor = "multi_thread")] - async fn backpressure_write_stream() { - // Stream can buffer up to 1k, plus one write chunk, before not - // accepting more input: - let (mut reader, writer) = simplex(1024); - let mut writer = AsyncWriteStream::new(writer); - - // Write enough to fill the simplex buffer: - let chunk = Bytes::from_static(&[0; 1024]); - let (len, state) = writer.write(chunk.clone()).unwrap(); - - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - - // turn the crank and it should be ready for writing again: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - // Now fill the buffer between here and the writer task: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - - // Try shoving even more down there, and it shouldnt accept more input: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, 0); - assert_eq!(state, StreamState::Open); - - // turn the crank and it should Not become ready for writing until we read something out. - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .err() - .expect("the writer should be not become ready"); - - // Still not ready from the .write interface either: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, 0); - assert_eq!(state, StreamState::Open); - - // There is 2k in the buffer. I should be able to read all of it out: - let mut buf = [0; 2048]; - reader.read_exact(&mut buf).await.unwrap(); - - // and no more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.read(&mut buf)) - .await - .err() - .expect("nothing more buffered in the system"); - - // Now the backpressure should be cleared, and an additional write should be accepted. - - // immediately ready for writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) - .await - .expect("the writer should be ready instantly") - .expect("ready is ok"); - - // and the write succeeds: - let (len, state) = writer.write(chunk.clone()).unwrap(); - assert_eq!(len, chunk.len()); - assert_eq!(state, StreamState::Open); - } } From eacfe9016b8c49503e72c664086bff986cea4f16 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:01:18 -0700 Subject: [PATCH 076/118] Make blocking_write actually block until everything is written --- crates/wasi/src/preview2/preview2/io.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 1df842464eea..c39b1e47a3fb 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -94,12 +94,19 @@ impl streams::Host for T { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let written = self.write(stream, bytes).await?; - self.table_mut() - .get_output_stream_mut(stream)? - .ready() - .await?; - Ok(written) + let s = self.table_mut().get_output_stream_mut(stream)?; + + let mut bytes = bytes::Bytes::from(bytes); + let mut nwritten: usize = 0; + loop { + s.ready().await?; + let (written, state) = HostOutputStream::write(s.as_mut(), bytes.clone())?; + let _ = bytes.split_to(written); + nwritten += written; + if bytes.is_empty() || state == StreamState::Closed { + return Ok((nwritten as u64, state.into())); + } + } } async fn skip( From 842bb3808857230cca0448cb1938a1dffc8a67be Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:01:32 -0700 Subject: [PATCH 077/118] Remove debug print --- crates/wasi/src/preview2/pipe.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index f6ae8e21a59f..77440d279046 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -234,7 +234,6 @@ impl AsyncWriteStream { use tokio::io::AsyncWriteExt; match receiver.recv().await { Some(mut bytes) => { - println!("got bytes: {:?}", bytes); while !bytes.is_empty() { match writer.write_buf(&mut bytes).await { Ok(0) => { From 56112b485859c9b7556b32617aeaf3d8bfc1801e Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:01:40 -0700 Subject: [PATCH 078/118] Adapter stdio should use blocking write Rust guests will panic if the write returns less than the number of bytes sent with stdio. --- crates/wasi-preview1-component-adapter/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs index 32c5b36d05f3..7d64bf080c58 100644 --- a/crates/wasi-preview1-component-adapter/src/lib.rs +++ b/crates/wasi-preview1-component-adapter/src/lib.rs @@ -1222,7 +1222,9 @@ pub unsafe extern "C" fn fd_write( streams::write(wasi_stream, bytes) } } else { - streams::write(wasi_stream, bytes) + // Use blocking writes on non-file streams (stdout, stderr, as sockets + // aren't currently used). + streams::blocking_write(wasi_stream, bytes) } .map_err(|_| ERRNO_IO)?; From 6d1711a4b79350724b4290c209d7ce62853e9e8b Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:22:53 -0700 Subject: [PATCH 079/118] Clean up implementations of {blocking_}write_zeros and skip --- crates/wasi/src/preview2/preview2/io.rs | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index c39b1e47a3fb..bf75f2b406c0 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -38,6 +38,8 @@ impl From for streams::StreamStatus { } } +const ZEROS: &[u8] = &[0; 4 * 1024 * 1024]; + #[async_trait::async_trait] impl streams::Host for T { async fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { @@ -127,12 +129,11 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let r = self.skip(stream, len).await?; self.table_mut() .get_input_stream_mut(stream)? .ready() .await?; - Ok(r) + self.skip(stream, len).await } async fn write_zeroes( @@ -140,13 +141,11 @@ impl streams::Host for T { stream: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - // let s = self.table_mut().get_output_stream_mut(stream)?; - // - // // TODO: the cast to usize should be fallible, use `.try_into()?` - // let bytes_written = s.write_zeroes(len as usize)?; - // - // Ok(bytes_written as u64) - todo!() + let s = self.table_mut().get_output_stream_mut(stream)?; + let mut bytes = bytes::Bytes::from_static(ZEROS); + bytes.truncate((len as usize).min(bytes.len())); + let (written, state) = HostOutputStream::write(s.as_mut(), bytes)?; + Ok((written as u64, state.into())) } async fn blocking_write_zeroes( @@ -154,12 +153,18 @@ impl streams::Host for T { stream: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let r = self.write_zeroes(stream, len).await?; - self.table_mut() - .get_output_stream_mut(stream)? - .ready() - .await?; - Ok(r) + let mut remaining = len as usize; + let s = self.table_mut().get_output_stream_mut(stream)?; + loop { + s.ready().await?; + let mut bytes = bytes::Bytes::from_static(ZEROS); + bytes.truncate(remaining.min(bytes.len())); + let (written, state) = HostOutputStream::write(s.as_mut(), bytes)?; + remaining -= written; + if remaining == 0 || state == StreamState::Closed { + return Ok((len - remaining as u64, state.into())); + } + } } async fn splice( From 0a721642ec780661948303fa18e323064bc8d895 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:23:50 -0700 Subject: [PATCH 080/118] Remove debug macro usage --- crates/wasi/src/preview2/pipe.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 77440d279046..84163cc3c1ac 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -293,7 +293,6 @@ impl HostOutputStream for AsyncWriteStream { fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), anyhow::Error> { use tokio::sync::mpsc::error::TryRecvError; - dbg!(&self.state); match self.state { Some(WriteState::Ready) => self.send(bytes), Some(WriteState::Pending) => match self.result_receiver.try_recv() { From 4babe7eb1f7fa49546361f1a672411e7abab79e0 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:31:26 -0700 Subject: [PATCH 081/118] Move EmptyStream to pipe, and split it into four variants Use EmptyInputStream and SinkOutputStream as the defaults for stdin and stdout/stderr respectively. --- crates/wasi/src/preview2/ctx.rs | 8 ++--- crates/wasi/src/preview2/pipe.rs | 57 +++++++++++++++++++++++++++++++ crates/wasi/src/preview2/stdio.rs | 26 -------------- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 7c0ea89128a7..6bf2448d8e22 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -1,7 +1,7 @@ use crate::preview2::{ clocks::{self, WasiClocks}, filesystem::{Dir, TableFsExt}, - random, stdio, + pipe, random, stdio, stream::{HostInputStream, HostOutputStream, TableStreamExt}, DirPerms, FilePerms, Table, }; @@ -33,9 +33,9 @@ impl WasiCtxBuilder { let insecure_random_seed = cap_rand::thread_rng(cap_rand::ambient_authority()).gen::(); Self { - stdin: Box::new(stdio::EmptyStream), - stdout: Box::new(stdio::EmptyStream), - stderr: Box::new(stdio::EmptyStream), + stdin: Box::new(pipe::EmptyInputStream), + stdout: Box::new(pipe::SinkOutputStream), + stderr: Box::new(pipe::SinkOutputStream), env: Vec::new(), args: Vec::new(), preopens: Vec::new(), diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 84163cc3c1ac..f19480258e53 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -359,6 +359,63 @@ impl HostOutputStream for AsyncWriteStream { } } +/// A stream that is never able to provide input. It stays open forever, but is never ready for +/// reading. +pub struct EmptyInputStream; + +#[async_trait::async_trait] +impl HostInputStream for EmptyInputStream { + fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { + Ok((Bytes::new(), StreamState::Open)) + } + + async fn ready(&mut self) -> Result<(), Error> { + futures::future::pending().await + } +} + +/// An output stream that consumes all input written to it, and is always ready. +pub struct SinkOutputStream; + +#[async_trait::async_trait] +impl HostOutputStream for SinkOutputStream { + fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { + Ok((buf.len(), StreamState::Open)) + } + + async fn ready(&mut self) -> Result<(), Error> { + Ok(()) + } +} + +/// A stream that is ready immediately, but will always report that it's closed. +pub struct ClosedInputStream; + +#[async_trait::async_trait] +impl HostInputStream for ClosedInputStream { + fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { + Ok((Bytes::new(), StreamState::Closed)) + } + + async fn ready(&mut self) -> Result<(), Error> { + Ok(()) + } +} + +/// An output stream that is always closed. +pub struct ClosedOutputStream; + +#[async_trait::async_trait] +impl HostOutputStream for ClosedOutputStream { + fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { + Ok((0, StreamState::Closed)) + } + + async fn ready(&mut self) -> Result<(), Error> { + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 154730545912..86f3f1f60dea 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -23,29 +23,3 @@ pub type Stderr = AsyncWriteStream; pub fn stderr() -> Stderr { AsyncWriteStream::new(tokio::io::stderr()) } - -pub struct EmptyStream; - -#[async_trait::async_trait] -impl HostInputStream for EmptyStream { - fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { - Ok((Bytes::new(), StreamState::Open)) - } - - async fn ready(&mut self) -> Result<(), Error> { - futures::future::pending().await - } -} - -#[async_trait::async_trait] -impl HostOutputStream for EmptyStream { - fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { - // Ok(buf.len() as u64) - todo!() - } - - async fn ready(&mut self) -> Result<(), Error> { - // This stream is always ready for writing. - Ok(()) - } -} From f1daf361cfc796c56d08b527a74d61e4436d4add Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 15:43:39 -0700 Subject: [PATCH 082/118] Add a big warning about resource lifetime tracking in pollables --- crates/wasi/src/preview2/poll.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index 4281820b266d..cbb48b3d46c8 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -23,6 +23,18 @@ pub enum HostPollable { /// Create a Future by calling a fn on another resource in the table. This /// indirection means the created Future can use a mut borrow of another /// resource in the Table (e.g. a stream) + /// + /// FIXME: we currently aren't tracking the lifetime of the resource along + /// with this entry, which means that this index could be occupied by something + /// unrelated by the time we poll it again. This is a crash vector, because + /// the [MakeFuture] would panic if the type of the index has changed, and + /// would yield undefined behavior otherwise. We'll likely fix this by making + /// the parent resources of a pollable clean up their pollable entries when + /// they are destroyed (e.g. the HostInputStream would track the pollables it + /// has created). + /// + /// WARNING: do not deploy this library to production until the above issue has + /// been fixed. TableEntry { index: u32, make_future: MakeFuture }, /// Create a future by calling an owned, static closure. This is used for /// pollables which do not share state with another resource in the Table From 1c4cff559cf95ad3f15ec150ce00274394369bf5 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 18 Jul 2023 17:02:41 -0700 Subject: [PATCH 083/118] Start working through changes to the filesystem implementation --- crates/wasi/src/preview2/mod.rs | 48 +++- crates/wasi/src/preview2/preview1/mod.rs | 106 +++++---- .../wasi/src/preview2/preview2/filesystem.rs | 222 ++++++++++-------- 3 files changed, 229 insertions(+), 147 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 3444839a7138..dbed3e21f527 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -36,10 +36,7 @@ pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; pub use self::poll::{HostPollable, TablePollableExt}; pub use self::random::{thread_rng, Deterministic}; -pub use self::stream::{ - HostInputStream, HostOutputStream, StreamState, - TableStreamExt, -}; +pub use self::stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}; pub use self::table::{Table, TableError}; pub use cap_fs_ext::SystemTimeSpec; pub use cap_rand::RngCore; @@ -52,14 +49,19 @@ pub mod bindings { interfaces: " import wasi:poll/poll import wasi:io/streams + import wasi:filesystem/filesystem ", tracing: true, trappable_error_type: { "streams"::"stream-error": Error, + "filesystem"::"error-code": Error, + }, + with: { + "wasi:clocks/wall-clock": crate::preview2::bindings::clocks::wall_clock, } }); } - pub use self::_internal::wasi::{io, poll}; + pub use self::_internal::wasi::{filesystem, io, poll}; impl From for io::streams::StreamError { fn from(_other: super::io::streams::StreamError) -> Self { @@ -68,6 +70,13 @@ pub mod bindings { } } + impl From for filesystem::filesystem::ErrorCode { + fn from(other: super::filesystem::filesystem::ErrorCode) -> Self { + match other { + } + } + } + impl From for io::streams::Error { fn from(other: super::io::streams::Error) -> Self { match other.downcast() { @@ -78,21 +87,40 @@ pub mod bindings { } } + pub(crate) mod _internal_clocks { + wasmtime::component::bindgen!({ + path: "wit", + interfaces: " + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + ", + tracing: true, + }); + } + pub use self::_internal_clocks::wasi::clocks; + pub(crate) mod _internal_io { wasmtime::component::bindgen!({ path: "wit", interfaces: " import wasi:poll/poll import wasi:io/streams + import wasi:filesystem/filesystem ", tracing: true, async: true, trappable_error_type: { "streams"::"stream-error": Error, + "filesystem"::"error-code": Error, + }, + with: { + "wasi:clocks/wall-clock": crate::preview2::bindings::clocks::wall_clock, } }); } - pub use self::_internal_io::wasi::{io, poll}; + pub use self::_internal_io::wasi::{filesystem, io, poll}; + pub(crate) mod _internal_rest { wasmtime::component::bindgen!({ path: "wit", @@ -100,7 +128,6 @@ pub mod bindings { import wasi:clocks/wall-clock import wasi:clocks/monotonic-clock import wasi:clocks/timezone - import wasi:filesystem/filesystem import wasi:random/random import wasi:random/insecure import wasi:random/insecure-seed @@ -117,11 +144,14 @@ pub mod bindings { "streams"::"stream-error": Error, }, with: { - "wasi:poll/poll": crate::preview2::bindings::poll::poll, - "wasi:io/streams": crate::preview2::bindings::io::streams + "wasi:clocks/wall-clock": crate::preview2::bindings::clocks::wall_clock, + "wasi:poll/poll": crate::preview2::bindings::poll::poll, + "wasi:io/streams": crate::preview2::bindings::io::streams, + "wasi:filesystem/filesystem": crate::preview2::bindings::filesystem::filesystem } }); } + pub use self::_internal_rest::wasi::*; } diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index 45b4f1c3f5b0..498fb01161f7 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -338,7 +338,15 @@ pub fn add_to_linker< // to this module. wiggle::from_witx!({ witx: ["$CARGO_MANIFEST_DIR/witx/wasi_snapshot_preview1.witx"], - async: { wasi_snapshot_preview1::{fd_close, fd_read, fd_pread, fd_write, fd_pwrite, poll_oneoff} }, + async: { + wasi_snapshot_preview1::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, errors: { errno => trappable Error }, }); @@ -760,7 +768,7 @@ impl< } #[instrument(skip(self))] - fn fd_advise( + async fn fd_advise( &mut self, fd: types::Fd, offset: types::Filesize, @@ -768,11 +776,13 @@ impl< advice: types::Advice, ) -> Result<(), types::Error> { let fd = self.get_file_fd(fd)?; - self.advise(fd, offset, len, advice.into()).map_err(|e| { - e.try_into() - .context("failed to call `advise`") - .unwrap_or_else(types::Error::trap) - }) + self.advise(fd, offset, len, advice.into()) + .await + .map_err(|e| { + e.try_into() + .context("failed to call `advise`") + .unwrap_or_else(types::Error::trap) + }) } /// Force the allocation of space in a file. @@ -810,6 +820,7 @@ impl< } Descriptor::File(File { fd, .. }) | Descriptor::PreopenDirectory((fd, _)) => self .drop_descriptor(fd) + .await .context("failed to call `drop-descriptor`"), } .map_err(types::Error::trap) @@ -818,9 +829,9 @@ impl< /// Synchronize the data of a file to disk. /// NOTE: This is similar to `fdatasync` in POSIX. #[instrument(skip(self))] - fn fd_datasync(&mut self, fd: types::Fd) -> Result<(), types::Error> { + async fn fd_datasync(&mut self, fd: types::Fd) -> Result<(), types::Error> { let fd = self.get_file_fd(fd)?; - self.sync_data(fd).map_err(|e| { + self.sync_data(fd).await.map_err(|e| { e.try_into() .context("failed to call `sync-data`") .unwrap_or_else(types::Error::trap) @@ -830,7 +841,7 @@ impl< /// Get the attributes of a file descriptor. /// NOTE: This returns similar flags to `fsync(fd, F_GETFL)` in POSIX, as well as additional fields. #[instrument(skip(self))] - fn fd_fdstat_get(&mut self, fd: types::Fd) -> Result { + async fn fd_fdstat_get(&mut self, fd: types::Fd) -> Result { let (fd, blocking, append) = match self.transact()?.get_descriptor(fd)? { Descriptor::Stdin(..) => { let fs_rights_base = types::Rights::FD_READ; @@ -861,13 +872,14 @@ impl< // TODO: use `try_join!` to poll both futures async, unfortunately that is not currently // possible, because `bindgen` generates methods with `&mut self` receivers. - let flags = self.get_flags(fd).map_err(|e| { + let flags = self.get_flags(fd).await.map_err(|e| { e.try_into() .context("failed to call `get-flags`") .unwrap_or_else(types::Error::trap) })?; let fs_filetype = self .get_type(fd) + .await .map_err(|e| { e.try_into() .context("failed to call `get-type`") @@ -945,7 +957,7 @@ impl< /// Return the attributes of an open file. #[instrument(skip(self))] - fn fd_filestat_get(&mut self, fd: types::Fd) -> Result { + async fn fd_filestat_get(&mut self, fd: types::Fd) -> Result { let desc = self.transact()?.get_descriptor(fd)?.clone(); match desc { Descriptor::Stdin(..) | Descriptor::Stdout(..) | Descriptor::Stderr(..) => { @@ -970,7 +982,7 @@ impl< data_access_timestamp, data_modification_timestamp, status_change_timestamp, - } = self.stat(fd).map_err(|e| { + } = self.stat(fd).await.map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -996,13 +1008,13 @@ impl< /// Adjust the size of an open file. If this increases the file's size, the extra bytes are filled with zeros. /// NOTE: This is similar to `ftruncate` in POSIX. #[instrument(skip(self))] - fn fd_filestat_set_size( + async fn fd_filestat_set_size( &mut self, fd: types::Fd, size: types::Filesize, ) -> Result<(), types::Error> { let fd = self.get_file_fd(fd)?; - self.set_size(fd, size).map_err(|e| { + self.set_size(fd, size).await.map_err(|e| { e.try_into() .context("failed to call `set-size`") .unwrap_or_else(types::Error::trap) @@ -1012,7 +1024,7 @@ impl< /// Adjust the timestamps of an open file or directory. /// NOTE: This is similar to `futimens` in POSIX. #[instrument(skip(self))] - fn fd_filestat_set_times( + async fn fd_filestat_set_times( &mut self, fd: types::Fd, atim: types::Timestamp, @@ -1031,7 +1043,7 @@ impl< )?; let fd = self.get_fd(fd)?; - self.set_times(fd, atim, mtim).map_err(|e| { + self.set_times(fd, atim, mtim).await.map_err(|e| { e.try_into() .context("failed to call `set-times`") .unwrap_or_else(types::Error::trap) @@ -1059,7 +1071,7 @@ impl< }; let pos = position.load(Ordering::Relaxed); - let stream = self.read_via_stream(fd, pos).map_err(|e| { + let stream = self.read_via_stream(fd, pos).await.map_err(|e| { e.try_into() .context("failed to call `read-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1118,7 +1130,7 @@ impl< return Ok(0) }; - let stream = self.read_via_stream(fd, offset).map_err(|e| { + let stream = self.read_via_stream(fd, offset).await.map_err(|e| { e.try_into() .context("failed to call `read-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1171,7 +1183,7 @@ impl< return Ok(0) }; let (stream, pos) = if append { - let stream = self.append_via_stream(fd).map_err(|e| { + let stream = self.append_via_stream(fd).await.map_err(|e| { e.try_into() .context("failed to call `append-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1179,7 +1191,7 @@ impl< (stream, 0) } else { let position = position.load(Ordering::Relaxed); - let stream = self.write_via_stream(fd, position).map_err(|e| { + let stream = self.write_via_stream(fd, position).await.map_err(|e| { e.try_into() .context("failed to call `write-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1228,7 +1240,7 @@ impl< let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - let stream = self.write_via_stream(fd, offset).map_err(|e| { + let stream = self.write_via_stream(fd, offset).await.map_err(|e| { e.try_into() .context("failed to call `write-via-stream`") .unwrap_or_else(types::Error::trap) @@ -1292,7 +1304,7 @@ impl< /// Move the offset of a file descriptor. /// NOTE: This is similar to `lseek` in POSIX. #[instrument(skip(self))] - fn fd_seek( + async fn fd_seek( &mut self, fd: types::Fd, offset: types::Filedelta, @@ -1310,7 +1322,7 @@ impl< .checked_add_signed(offset) .ok_or(types::Errno::Inval)?, types::Whence::End => { - let filesystem::DescriptorStat { size, .. } = self.stat(fd).map_err(|e| { + let filesystem::DescriptorStat { size, .. } = self.stat(fd).await.map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -1326,9 +1338,9 @@ impl< /// Synchronize the data and metadata of a file to disk. /// NOTE: This is similar to `fsync` in POSIX. #[instrument(skip(self))] - fn fd_sync(&mut self, fd: types::Fd) -> Result<(), types::Error> { + async fn fd_sync(&mut self, fd: types::Fd) -> Result<(), types::Error> { let fd = self.get_file_fd(fd)?; - self.sync(fd).map_err(|e| { + self.sync(fd).await.map_err(|e| { e.try_into() .context("failed to call `sync`") .unwrap_or_else(types::Error::trap) @@ -1347,7 +1359,7 @@ impl< } #[instrument(skip(self))] - fn fd_readdir<'a>( + async fn fd_readdir<'a>( &mut self, fd: types::Fd, buf: &GuestPtr<'a, u8>, @@ -1355,14 +1367,14 @@ impl< cookie: types::Dircookie, ) -> Result { let fd = self.get_dir_fd(fd)?; - let stream = self.read_directory(fd).map_err(|e| { + let stream = self.read_directory(fd).await.map_err(|e| { e.try_into() .context("failed to call `read-directory`") .unwrap_or_else(types::Error::trap) })?; let filesystem::DescriptorStat { inode: fd_inode, .. - } = self.stat(fd).map_err(|e| { + } = self.stat(fd).await.map_err(|e| { e.try_into() .context("failed to call `stat`") .unwrap_or_else(types::Error::trap) @@ -1457,14 +1469,14 @@ impl< } #[instrument(skip(self))] - fn path_create_directory<'a>( + async fn path_create_directory<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - self.create_directory_at(dirfd, path).map_err(|e| { + self.create_directory_at(dirfd, path).await.map_err(|e| { e.try_into() .context("failed to call `create-directory-at`") .unwrap_or_else(types::Error::trap) @@ -1474,7 +1486,7 @@ impl< /// Return the attributes of a file or directory. /// NOTE: This is similar to `stat` in POSIX. #[instrument(skip(self))] - fn path_filestat_get<'a>( + async fn path_filestat_get<'a>( &mut self, dirfd: types::Fd, flags: types::Lookupflags, @@ -1491,7 +1503,7 @@ impl< data_access_timestamp, data_modification_timestamp, status_change_timestamp, - } = self.stat_at(dirfd, flags.into(), path).map_err(|e| { + } = self.stat_at(dirfd, flags.into(), path).await.map_err(|e| { e.try_into() .context("failed to call `stat-at`") .unwrap_or_else(types::Error::trap) @@ -1515,7 +1527,7 @@ impl< /// Adjust the timestamps of a file or directory. /// NOTE: This is similar to `utimensat` in POSIX. #[instrument(skip(self))] - fn path_filestat_set_times<'a>( + async fn path_filestat_set_times<'a>( &mut self, dirfd: types::Fd, flags: types::Lookupflags, @@ -1538,6 +1550,7 @@ impl< let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; self.set_times_at(dirfd, flags.into(), path, atim, mtim) + .await .map_err(|e| { e.try_into() .context("failed to call `set-times-at`") @@ -1548,7 +1561,7 @@ impl< /// Create a hard link. /// NOTE: This is similar to `linkat` in POSIX. #[instrument(skip(self))] - fn path_link<'a>( + async fn path_link<'a>( &mut self, src_fd: types::Fd, src_flags: types::Lookupflags, @@ -1561,6 +1574,7 @@ impl< let src_path = read_string(src_path)?; let target_path = read_string(target_path)?; self.link_at(src_fd, src_flags.into(), src_path, target_fd, target_path) + .await .map_err(|e| { e.try_into() .context("failed to call `link-at`") @@ -1571,7 +1585,7 @@ impl< /// Open a file or directory. /// NOTE: This is similar to `openat` in POSIX. #[instrument(skip(self))] - fn path_open<'a>( + async fn path_open<'a>( &mut self, dirfd: types::Fd, dirflags: types::Lookupflags, @@ -1619,6 +1633,7 @@ impl< flags, filesystem::Modes::READABLE | filesystem::Modes::WRITABLE, ) + .await .map_err(|e| { e.try_into() .context("failed to call `open-at`") @@ -1636,7 +1651,7 @@ impl< /// Read the contents of a symbolic link. /// NOTE: This is similar to `readlinkat` in POSIX. #[instrument(skip(self))] - fn path_readlink<'a>( + async fn path_readlink<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, @@ -1645,7 +1660,7 @@ impl< ) -> Result { let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - let mut path = self.readlink_at(dirfd, path).map_err(|e| { + let mut path = self.readlink_at(dirfd, path).await.map_err(|e| { e.try_into() .context("failed to call `readlink-at`") .unwrap_or_else(types::Error::trap) @@ -1660,14 +1675,14 @@ impl< } #[instrument(skip(self))] - fn path_remove_directory<'a>( + async fn path_remove_directory<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { let dirfd = self.get_dir_fd(dirfd)?; let path = read_string(path)?; - self.remove_directory_at(dirfd, path).map_err(|e| { + self.remove_directory_at(dirfd, path).await.map_err(|e| { e.try_into() .context("failed to call `remove-directory-at`") .unwrap_or_else(types::Error::trap) @@ -1677,7 +1692,7 @@ impl< /// Rename a file or directory. /// NOTE: This is similar to `renameat` in POSIX. #[instrument(skip(self))] - fn path_rename<'a>( + async fn path_rename<'a>( &mut self, src_fd: types::Fd, src_path: &GuestPtr<'a, str>, @@ -1689,6 +1704,7 @@ impl< let src_path = read_string(src_path)?; let dest_path = read_string(dest_path)?; self.rename_at(src_fd, src_path, dest_fd, dest_path) + .await .map_err(|e| { e.try_into() .context("failed to call `rename-at`") @@ -1697,7 +1713,7 @@ impl< } #[instrument(skip(self))] - fn path_symlink<'a>( + async fn path_symlink<'a>( &mut self, src_path: &GuestPtr<'a, str>, dirfd: types::Fd, @@ -1706,7 +1722,7 @@ impl< let dirfd = self.get_dir_fd(dirfd)?; let src_path = read_string(src_path)?; let dest_path = read_string(dest_path)?; - self.symlink_at(dirfd, src_path, dest_path).map_err(|e| { + self.symlink_at(dirfd, src_path, dest_path).await.map_err(|e| { e.try_into() .context("failed to call `symlink-at`") .unwrap_or_else(types::Error::trap) @@ -1714,14 +1730,14 @@ impl< } #[instrument(skip(self))] - fn path_unlink_file<'a>( + async fn path_unlink_file<'a>( &mut self, dirfd: types::Fd, path: &GuestPtr<'a, str>, ) -> Result<(), types::Error> { let dirfd = self.get_dir_fd(dirfd)?; let path = path.as_cow().map_err(|_| types::Errno::Inval)?.to_string(); - self.unlink_file_at(dirfd, path).map_err(|e| { + self.unlink_file_at(dirfd, path).await.map_err(|e| { e.try_into() .context("failed to call `unlink-file-at`") .unwrap_or_else(types::Error::trap) diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 65f0ed313a81..9b620e265d39 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -2,9 +2,8 @@ use crate::preview2::bindings::clocks::wall_clock; use crate::preview2::bindings::filesystem::filesystem; use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::{Dir, File, TableFsExt}; -use crate::preview2::{ - block_in_place, DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView, -}; +use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView}; +use tokio::task::spawn_blocking; use filesystem::ErrorCode; @@ -17,8 +16,15 @@ impl From for filesystem::Error { } } +impl From for filesystem::Error { + fn from(error: tokio::task::JoinError) -> Self { + Self::trap(anyhow::anyhow!(error)) + } +} + +#[async_trait::async_trait] impl filesystem::Host for T { - fn advise( + async fn advise( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -38,15 +44,15 @@ impl filesystem::Host for T { }; let f = self.table().get_file(fd)?; - block_in_place(|| f.file.advise(offset, len, advice))?; + spawn_blocking(|| f.file.advise(offset, len, advice)).await??; Ok(()) } - fn sync_data(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn sync_data(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match block_in_place(|| f.file.sync_data()) { + match spawn_blocking(|| f.file.sync_data()).await? { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -62,23 +68,20 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - block_in_place(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_data()?)) + spawn_blocking(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_data()?)).await? } else { Err(ErrorCode::BadDescriptor.into()) } } - fn get_flags( + async fn get_flags( &mut self, fd: filesystem::Descriptor, ) -> Result { - use cap_std::io_lifetimes::AsFilelike; use filesystem::DescriptorFlags; use system_interface::fs::{FdFlags, GetSetFdFlags}; - fn get_from_fdflags(f: impl AsFilelike) -> std::io::Result { - let f = f.as_filelike(); - let flags = block_in_place(|| f.get_fd_flags())?; + fn get_from_fdflags(flags: FdFlags) -> DescriptorFlags { let mut out = DescriptorFlags::empty(); if flags.contains(FdFlags::DSYNC) { out |= DescriptorFlags::REQUESTED_WRITE_SYNC; @@ -89,13 +92,14 @@ impl filesystem::Host for T { if flags.contains(FdFlags::SYNC) { out |= DescriptorFlags::FILE_INTEGRITY_SYNC; } - Ok(out) + out } let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - let mut flags = get_from_fdflags(&*f.file)?; + let flags = spawn_blocking(|| f.file.get_fd_flags()).await??; + let mut flags = get_from_fdflags(flags); if f.perms.contains(FilePerms::READ) { flags |= DescriptorFlags::READ; } @@ -105,7 +109,8 @@ impl filesystem::Host for T { Ok(flags) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - let mut flags = get_from_fdflags(&d.dir)?; + let flags = spawn_blocking(|| d.dir.get_fd_flags()).await??; + let mut flags = get_from_fdflags(flags); if d.perms.contains(DirPerms::READ) { flags |= DescriptorFlags::READ; } @@ -118,7 +123,7 @@ impl filesystem::Host for T { } } - fn get_type( + async fn get_type( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -126,7 +131,7 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; - let meta = block_in_place(|| f.file.metadata())?; + let meta = spawn_blocking(|| f.file.metadata()).await??; Ok(descriptortype_from(meta.file_type())) } else if table.is_dir(fd) { Ok(filesystem::DescriptorType::Directory) @@ -135,7 +140,7 @@ impl filesystem::Host for T { } } - fn set_size( + async fn set_size( &mut self, fd: filesystem::Descriptor, size: filesystem::Filesize, @@ -144,11 +149,11 @@ impl filesystem::Host for T { if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; } - block_in_place(|| f.file.set_len(size))?; + spawn_blocking(|| f.file.set_len(size)).await??; Ok(()) } - fn set_times( + async fn set_times( &mut self, fd: filesystem::Descriptor, atim: filesystem::NewTimestamp, @@ -164,7 +169,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - block_in_place(|| f.file.set_times(atim, mtim))?; + spawn_blocking(|| f.file.set_times(atim, mtim)).await??; Ok(()) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; @@ -173,14 +178,14 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - block_in_place(|| d.dir.set_times(atim, mtim))?; + spawn_blocking(|| d.dir.set_times(atim, mtim)).await??; Ok(()) } else { Err(ErrorCode::BadDescriptor.into()) } } - fn read( + async fn read( &mut self, fd: filesystem::Descriptor, len: filesystem::Filesize, @@ -197,10 +202,13 @@ impl filesystem::Host for T { } let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; - let (bytes_read, state) = crate::preview2::filesystem::read_result(block_in_place(|| { - f.file - .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset) - }))?; + let (bytes_read, state) = crate::preview2::filesystem::read_result( + spawn_blocking(|| { + f.file + .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset) + }) + .await?, + )?; buffer.truncate( bytes_read @@ -211,7 +219,7 @@ impl filesystem::Host for T { Ok((buffer, state.is_closed())) } - fn write( + async fn write( &mut self, fd: filesystem::Descriptor, buf: Vec, @@ -227,12 +235,12 @@ impl filesystem::Host for T { } let bytes_written = - block_in_place(|| f.file.write_vectored_at(&[IoSlice::new(&buf)], offset))?; + spawn_blocking(|| f.file.write_vectored_at(&[IoSlice::new(&buf)], offset)).await??; Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) } - fn read_directory( + async fn read_directory( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -254,17 +262,30 @@ impl filesystem::Host for T { } } - let entries = block_in_place(|| d.dir.entries())?.map(|entry| { - let entry = entry?; - let meta = block_in_place(|| entry.full_metadata())?; - let inode = Some(meta.ino()); - let type_ = descriptortype_from(meta.file_type()); - let name = entry - .file_name() - .into_string() - .map_err(|_| ReaddirError::IllegalSequence)?; - Ok(filesystem::DirectoryEntry { inode, type_, name }) - }); + let entries = spawn_blocking(|| { + // Both `entries` and `full_metadata` perform syscalls, which is why they are done + // within this `spawn_blocking` call, rather than delay calculating the full metadata + // for entries when they're demanded later in the iterator chain. + Ok::<_, std::io::Error>( + d.dir + .entries()? + .map(|entry| { + let entry = entry?; + let meta = entry.full_metadata()?; + let inode = Some(meta.ino()); + let type_ = descriptortype_from(meta.file_type()); + let name = entry + .file_name() + .into_string() + .map_err(|_| ReaddirError::IllegalSequence)?; + Ok(filesystem::DirectoryEntry { inode, type_, name }) + }) + .collect::>>(), + ) + }) + .await?? + .into_iter(); + // On windows, filter out files like `C:\DumpStack.log.tmp` which we // can't get full metadata for. #[cfg(windows)] @@ -287,7 +308,7 @@ impl filesystem::Host for T { Ok(table.push_readdir(ReaddirIterator::new(entries))?) } - fn read_directory_entry( + async fn read_directory_entry( &mut self, stream: filesystem::DirectoryEntryStream, ) -> Result, filesystem::Error> { @@ -296,7 +317,7 @@ impl filesystem::Host for T { readdir.next() } - fn drop_directory_entry_stream( + async fn drop_directory_entry_stream( &mut self, stream: filesystem::DirectoryEntryStream, ) -> anyhow::Result<()> { @@ -304,11 +325,11 @@ impl filesystem::Host for T { Ok(()) } - fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match block_in_place(|| f.file.sync_all()) { + match spawn_blocking(|| f.file.sync_all()).await? { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -324,13 +345,13 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - block_in_place(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_all()?)) + spawn_blocking(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_all()?)).await? } else { Err(ErrorCode::BadDescriptor.into()) } } - fn create_directory_at( + async fn create_directory_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -340,11 +361,11 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - block_in_place(|| d.dir.create_dir(&path))?; + spawn_blocking(|| d.dir.create_dir(&path)).await??; Ok(()) } - fn stat( + async fn stat( &mut self, fd: filesystem::Descriptor, ) -> Result { @@ -352,19 +373,19 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = block_in_place(|| f.file.metadata())?; + let meta = spawn_blocking(|| f.file.metadata()).await??; Ok(descriptorstat_from(meta)) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = block_in_place(|| d.dir.dir_metadata())?; + let meta = spawn_blocking(|| d.dir.dir_metadata()).await??; Ok(descriptorstat_from(meta)) } else { Err(ErrorCode::BadDescriptor.into()) } } - fn stat_at( + async fn stat_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -377,14 +398,14 @@ impl filesystem::Host for T { } let meta = if symlink_follow(path_flags) { - block_in_place(|| d.dir.metadata(&path))? + spawn_blocking(|| d.dir.metadata(&path)).await?? } else { - block_in_place(|| d.dir.symlink_metadata(&path))? + spawn_blocking(|| d.dir.symlink_metadata(&path)).await?? }; Ok(descriptorstat_from(meta)) } - fn set_times_at( + async fn set_times_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -402,26 +423,28 @@ impl filesystem::Host for T { let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; if symlink_follow(path_flags) { - block_in_place(|| { + spawn_blocking(|| { d.dir.set_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), mtim.map(cap_fs_ext::SystemTimeSpec::from_std), ) - })?; + }) + .await??; } else { - block_in_place(|| { + spawn_blocking(|| { d.dir.set_symlink_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), mtim.map(cap_fs_ext::SystemTimeSpec::from_std), ) - })?; + }) + .await??; } Ok(()) } - fn link_at( + async fn link_at( &mut self, fd: filesystem::Descriptor, // TODO delete the path flags from this function @@ -442,11 +465,11 @@ impl filesystem::Host for T { if symlink_follow(old_path_flags) { return Err(ErrorCode::Invalid.into()); } - block_in_place(|| old_dir.dir.hard_link(&old_path, &new_dir.dir, &new_path))?; + spawn_blocking(|| old_dir.dir.hard_link(&old_path, &new_dir.dir, &new_path)).await??; Ok(()) } - fn open_at( + async fn open_at( &mut self, fd: filesystem::Descriptor, path_flags: filesystem::PathFlags, @@ -524,9 +547,9 @@ impl filesystem::Host for T { Err(ErrorCode::Invalid)?; } } - let mut opened = block_in_place(|| d.dir.open_with(&path, &opts))?; + let mut opened = spawn_blocking(|| d.dir.open_with(&path, &opts)).await??; - if block_in_place(|| opened.metadata())?.is_dir() { + if spawn_blocking(|| opened.metadata()).await??.is_dir() { Ok(table.push_dir(Dir::new( cap_std::fs::Dir::from_std_file(opened.into_std()), d.perms, @@ -537,25 +560,29 @@ impl filesystem::Host for T { } else { // FIXME cap-std needs a nonblocking open option so that files reads and writes // are nonblocking. Instead we set it after opening here: - block_in_place(|| { + spawn_blocking(|| { let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; opened.set_fd_flags(set_fd_flags)?; Ok::<(), filesystem::Error>(()) - })?; + }) + .await??; Ok(table.push_file(File::new(opened, mask_file_perms(d.file_perms, flags)))?) } } - fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { + async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { let table = self.table_mut(); - if block_in_place(|| table.delete_file(fd)).is_err() { - block_in_place(|| table.delete_dir(fd))?; - } - Ok(()) + spawn_blocking(|| { + if table.delete_file(fd).is_err() { + table.delete_dir(fd)?; + } + Ok(()) + }) + .await? } - fn readlink_at( + async fn readlink_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -565,14 +592,14 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::READ) { return Err(ErrorCode::NotPermitted.into()); } - let link = block_in_place(|| d.dir.read_link(&path))?; + let link = spawn_blocking(|| d.dir.read_link(&path)).await??; Ok(link .into_os_string() .into_string() .map_err(|_| ErrorCode::IllegalByteSequence)?) } - fn remove_directory_at( + async fn remove_directory_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -582,10 +609,10 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(block_in_place(|| d.dir.remove_dir(&path))?) + Ok(spawn_blocking(|| d.dir.remove_dir(&path)).await??) } - fn rename_at( + async fn rename_at( &mut self, fd: filesystem::Descriptor, old_path: String, @@ -601,11 +628,11 @@ impl filesystem::Host for T { if !new_dir.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - block_in_place(|| old_dir.dir.rename(&old_path, &new_dir.dir, &new_path))?; + spawn_blocking(|| old_dir.dir.rename(&old_path, &new_dir.dir, &new_path)).await??; Ok(()) } - fn symlink_at( + async fn symlink_at( &mut self, fd: filesystem::Descriptor, src_path: String, @@ -620,11 +647,11 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - block_in_place(|| d.dir.symlink(&src_path, &dest_path))?; + spawn_blocking(|| d.dir.symlink(&src_path, &dest_path)).await??; Ok(()) } - fn unlink_file_at( + async fn unlink_file_at( &mut self, fd: filesystem::Descriptor, path: String, @@ -636,11 +663,11 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - block_in_place(|| d.dir.remove_file_or_symlink(&path))?; + spawn_blocking(|| d.dir.remove_file_or_symlink(&path)).await??; Ok(()) } - fn access_at( + async fn access_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -650,7 +677,7 @@ impl filesystem::Host for T { todo!("filesystem access_at is not implemented") } - fn change_file_permissions_at( + async fn change_file_permissions_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -660,7 +687,7 @@ impl filesystem::Host for T { todo!("filesystem change_file_permissions_at is not implemented") } - fn change_directory_permissions_at( + async fn change_directory_permissions_at( &mut self, _fd: filesystem::Descriptor, _path_flags: filesystem::PathFlags, @@ -670,27 +697,36 @@ impl filesystem::Host for T { todo!("filesystem change_directory_permissions_at is not implemented") } - fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!("filesystem lock_shared is not implemented") } - fn lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn lock_exclusive( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { todo!("filesystem lock_exclusive is not implemented") } - fn try_lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn try_lock_shared( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { todo!("filesystem try_lock_shared is not implemented") } - fn try_lock_exclusive(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn try_lock_exclusive( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { todo!("filesystem try_lock_exclusive is not implemented") } - fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + async fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { todo!("filesystem unlock is not implemented") } - fn read_via_stream( + async fn read_via_stream( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -713,7 +749,7 @@ impl filesystem::Host for T { Ok(index) } - fn write_via_stream( + async fn write_via_stream( &mut self, fd: filesystem::Descriptor, offset: filesystem::Filesize, @@ -737,7 +773,7 @@ impl filesystem::Host for T { Ok(index) } - fn append_via_stream( + async fn append_via_stream( &mut self, fd: filesystem::Descriptor, ) -> Result { From 51fae7d32125dba3fe5e537c743c519897f151b6 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 19 Jul 2023 09:21:03 -0700 Subject: [PATCH 084/118] Remove todos in the filesystem implementation --- crates/wasi/src/preview2/filesystem.rs | 43 ++++++++++++++++---------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index cb97ee86b754..83fc93523858 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,7 +1,7 @@ use crate::preview2::{ block_in_place, HostInputStream, HostOutputStream, StreamState, Table, TableError, }; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use std::sync::Arc; bitflags::bitflags! { @@ -100,11 +100,14 @@ impl FileInputStream { #[async_trait::async_trait] impl HostInputStream for FileInputStream { fn read(&mut self, size: usize) -> anyhow::Result<(Bytes, StreamState)> { - // use system_interface::fs::FileIoExt; - // let (n, end) = read_result(block_in_place(|| self.file.read_at(buf, self.position)))?; - // self.position = self.position.wrapping_add(n); - // Ok((n, end)) - todo!() + use system_interface::fs::FileIoExt; + let mut buf = BytesMut::zeroed(size); + let (n, state) = read_result(block_in_place(|| { + self.file.read_at(&mut buf, self.position) + }))?; + buf.truncate(n); + self.position += n as u64; + Ok((buf.freeze(), state)) } async fn ready(&mut self) -> anyhow::Result<()> { Ok(()) // Always immediately ready - file reads cannot block @@ -113,15 +116,25 @@ impl HostInputStream for FileInputStream { pub(crate) fn read_result( r: Result, -) -> Result<(u64, StreamState), std::io::Error> { +) -> Result<(usize, StreamState), std::io::Error> { match r { Ok(0) => Ok((0, StreamState::Closed)), - Ok(n) => Ok((n as u64, StreamState::Open)), + Ok(n) => Ok((n, StreamState::Open)), Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), Err(e) => Err(e), } } +pub(crate) fn write_result( + r: Result, +) -> Result<(usize, StreamState), std::io::Error> { + match r { + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n, StreamState::Open)), + Err(e) => Err(e), + } +} + pub(crate) struct FileOutputStream { file: Arc, position: u64, @@ -136,11 +149,10 @@ impl FileOutputStream { impl HostOutputStream for FileOutputStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { - // use system_interface::fs::FileIoExt; - // let n = block_in_place(|| self.file.write_at(buf, self.position))? as i64 as u64; - // self.position = self.position.wrapping_add(n); - // Ok(n) - todo!() + use system_interface::fs::FileIoExt; + let (n, state) = write_result(block_in_place(|| self.file.write_at(&buf, self.position)))?; + self.position += n as u64; + Ok((n, state)) } async fn ready(&mut self) -> anyhow::Result<()> { @@ -161,9 +173,8 @@ impl FileAppendStream { impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { - // use system_interface::fs::FileIoExt; - // Ok(block_in_place(|| self.file.append(buf))? as i64 as u64) - todo!() + use system_interface::fs::FileIoExt; + Ok(write_result(block_in_place(|| self.file.append(&buf)))?) } async fn ready(&mut self) -> anyhow::Result<()> { From 0f1d7ff8198c30e414782c551dc1b0b484390498 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 19 Jul 2023 12:09:17 -0700 Subject: [PATCH 085/118] Avoid lifetime errors by moving blocking operations to File and Dir --- crates/wasi/src/preview2/filesystem.rs | 22 ++- crates/wasi/src/preview2/mod.rs | 6 + .../wasi/src/preview2/preview2/filesystem.rs | 187 +++++++++--------- 3 files changed, 125 insertions(+), 90 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 83fc93523858..09525208700b 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -23,6 +23,15 @@ impl File { perms, } } + + pub(crate) async fn block(&self, body: F) -> R + where + F: FnOnce(&cap_std::fs::File) -> R + Send + 'static, + R: Send + 'static, + { + let f = self.file.clone(); + tokio::task::spawn_blocking(move || body(&f)).await.unwrap() + } } pub(crate) trait TableFsExt { fn push_file(&mut self, file: File) -> Result; @@ -72,7 +81,7 @@ bitflags::bitflags! { } pub(crate) struct Dir { - pub dir: cap_std::fs::Dir, + pub dir: Arc, pub perms: DirPerms, pub file_perms: FilePerms, } @@ -80,11 +89,20 @@ pub(crate) struct Dir { impl Dir { pub fn new(dir: cap_std::fs::Dir, perms: DirPerms, file_perms: FilePerms) -> Self { Dir { - dir, + dir: Arc::new(dir), perms, file_perms, } } + + pub(crate) async fn block(&self, body: F) -> R + where + F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static, + R: Send + 'static, + { + let d = self.dir.clone(); + tokio::task::spawn_blocking(move || body(&d)).await.unwrap() + } } pub(crate) struct FileInputStream { diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index dbed3e21f527..29155dbd570b 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -72,7 +72,13 @@ pub mod bindings { impl From for filesystem::filesystem::ErrorCode { fn from(other: super::filesystem::filesystem::ErrorCode) -> Self { + use super::filesystem::filesystem::ErrorCode; match other { + ErrorCode::Access => Self::Access, + ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::Already => Self::Already, + ErrorCode::BadDescriptor => Self::BadDescriptor, + ErrorCode::Busy => Self::Busy, } } } diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 9b620e265d39..320c567ae95b 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -3,7 +3,6 @@ use crate::preview2::bindings::filesystem::filesystem; use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::{Dir, File, TableFsExt}; use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView}; -use tokio::task::spawn_blocking; use filesystem::ErrorCode; @@ -44,7 +43,7 @@ impl filesystem::Host for T { }; let f = self.table().get_file(fd)?; - spawn_blocking(|| f.file.advise(offset, len, advice)).await??; + f.block(move |f| f.advise(offset, len, advice)).await?; Ok(()) } @@ -52,7 +51,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match spawn_blocking(|| f.file.sync_data()).await? { + match f.block(|f| f.sync_data()).await { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -68,7 +67,8 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - spawn_blocking(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_data()?)).await? + d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_data()?)) + .await } else { Err(ErrorCode::BadDescriptor.into()) } @@ -98,7 +98,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - let flags = spawn_blocking(|| f.file.get_fd_flags()).await??; + let flags = f.block(|f| f.get_fd_flags()).await?; let mut flags = get_from_fdflags(flags); if f.perms.contains(FilePerms::READ) { flags |= DescriptorFlags::READ; @@ -109,7 +109,7 @@ impl filesystem::Host for T { Ok(flags) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - let flags = spawn_blocking(|| d.dir.get_fd_flags()).await??; + let flags = d.block(|d| d.get_fd_flags()).await?; let mut flags = get_from_fdflags(flags); if d.perms.contains(DirPerms::READ) { flags |= DescriptorFlags::READ; @@ -131,7 +131,7 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; - let meta = spawn_blocking(|| f.file.metadata()).await??; + let meta = f.block(|f| f.metadata()).await?; Ok(descriptortype_from(meta.file_type())) } else if table.is_dir(fd) { Ok(filesystem::DescriptorType::Directory) @@ -149,7 +149,7 @@ impl filesystem::Host for T { if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; } - spawn_blocking(|| f.file.set_len(size)).await??; + f.block(|f| f.set_len(size)).await?; Ok(()) } @@ -169,7 +169,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - spawn_blocking(|| f.file.set_times(atim, mtim)).await??; + f.block(|f| f.set_times(atim, mtim)).await?; Ok(()) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; @@ -178,7 +178,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - spawn_blocking(|| d.dir.set_times(atim, mtim)).await??; + d.block(|d| d.set_times(atim, mtim)).await?; Ok(()) } else { Err(ErrorCode::BadDescriptor.into()) @@ -203,11 +203,8 @@ impl filesystem::Host for T { let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; let (bytes_read, state) = crate::preview2::filesystem::read_result( - spawn_blocking(|| { - f.file - .read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset) - }) - .await?, + f.block(|f| f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)) + .await, )?; buffer.truncate( @@ -234,8 +231,9 @@ impl filesystem::Host for T { return Err(ErrorCode::NotPermitted.into()); } - let bytes_written = - spawn_blocking(|| f.file.write_vectored_at(&[IoSlice::new(&buf)], offset)).await??; + let bytes_written = f + .block(|f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) + .await?; Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) } @@ -262,29 +260,29 @@ impl filesystem::Host for T { } } - let entries = spawn_blocking(|| { - // Both `entries` and `full_metadata` perform syscalls, which is why they are done - // within this `spawn_blocking` call, rather than delay calculating the full metadata - // for entries when they're demanded later in the iterator chain. - Ok::<_, std::io::Error>( - d.dir - .entries()? - .map(|entry| { - let entry = entry?; - let meta = entry.full_metadata()?; - let inode = Some(meta.ino()); - let type_ = descriptortype_from(meta.file_type()); - let name = entry - .file_name() - .into_string() - .map_err(|_| ReaddirError::IllegalSequence)?; - Ok(filesystem::DirectoryEntry { inode, type_, name }) - }) - .collect::>>(), - ) - }) - .await?? - .into_iter(); + let entries = d + .block(|d| { + // Both `entries` and `full_metadata` perform syscalls, which is why they are done + // within this `block` call, rather than delay calculating the full metadata + // for entries when they're demanded later in the iterator chain. + Ok::<_, std::io::Error>( + d.entries()? + .map(|entry| { + let entry = entry?; + let meta = entry.full_metadata()?; + let inode = Some(meta.ino()); + let type_ = descriptortype_from(meta.file_type()); + let name = entry + .file_name() + .into_string() + .map_err(|_| ReaddirError::IllegalSequence)?; + Ok(filesystem::DirectoryEntry { inode, type_, name }) + }) + .collect::>>(), + ) + }) + .await? + .into_iter(); // On windows, filter out files like `C:\DumpStack.log.tmp` which we // can't get full metadata for. @@ -329,7 +327,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match spawn_blocking(|| f.file.sync_all()).await? { + match f.block(|f| f.sync_all()).await { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -345,7 +343,8 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - spawn_blocking(|| Ok(d.dir.open(std::path::Component::CurDir)?.sync_all()?)).await? + d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_all()?)) + .await } else { Err(ErrorCode::BadDescriptor.into()) } @@ -361,7 +360,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - spawn_blocking(|| d.dir.create_dir(&path)).await??; + d.block(|d| d.create_dir(&path)).await?; Ok(()) } @@ -373,12 +372,12 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = spawn_blocking(|| f.file.metadata()).await??; + let meta = f.block(|f| f.metadata()).await?; Ok(descriptorstat_from(meta)) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = spawn_blocking(|| d.dir.dir_metadata()).await??; + let meta = d.block(|d| d.dir_metadata()).await?; Ok(descriptorstat_from(meta)) } else { Err(ErrorCode::BadDescriptor.into()) @@ -398,9 +397,9 @@ impl filesystem::Host for T { } let meta = if symlink_follow(path_flags) { - spawn_blocking(|| d.dir.metadata(&path)).await?? + d.block(|d| d.metadata(&path)).await? } else { - spawn_blocking(|| d.dir.symlink_metadata(&path)).await?? + d.block(|d| d.symlink_metadata(&path)).await? }; Ok(descriptorstat_from(meta)) } @@ -423,23 +422,23 @@ impl filesystem::Host for T { let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; if symlink_follow(path_flags) { - spawn_blocking(|| { - d.dir.set_times( + d.block(|d| { + d.set_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), mtim.map(cap_fs_ext::SystemTimeSpec::from_std), ) }) - .await??; + .await?; } else { - spawn_blocking(|| { - d.dir.set_symlink_times( + d.block(|d| { + d.set_symlink_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), mtim.map(cap_fs_ext::SystemTimeSpec::from_std), ) }) - .await??; + .await?; } Ok(()) } @@ -465,7 +464,9 @@ impl filesystem::Host for T { if symlink_follow(old_path_flags) { return Err(ErrorCode::Invalid.into()); } - spawn_blocking(|| old_dir.dir.hard_link(&old_path, &new_dir.dir, &new_path)).await??; + old_dir + .block(|d| d.hard_link(&old_path, &new_dir.dir, &new_path)) + .await?; Ok(()) } @@ -547,39 +548,52 @@ impl filesystem::Host for T { Err(ErrorCode::Invalid)?; } } - let mut opened = spawn_blocking(|| d.dir.open_with(&path, &opts)).await??; - - if spawn_blocking(|| opened.metadata()).await??.is_dir() { - Ok(table.push_dir(Dir::new( - cap_std::fs::Dir::from_std_file(opened.into_std()), - d.perms, - d.file_perms, - ))?) - } else if oflags.contains(OpenFlags::DIRECTORY) { - Err(ErrorCode::NotDirectory)? - } else { - // FIXME cap-std needs a nonblocking open option so that files reads and writes - // are nonblocking. Instead we set it after opening here: - spawn_blocking(|| { - let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; - opened.set_fd_flags(set_fd_flags)?; - Ok::<(), filesystem::Error>(()) + + enum OpenResult { + Dir(cap_std::fs::Dir), + File(cap_std::fs::File), + NotDir, + } + + let opened = d + .block::<_, std::io::Result>(|d| { + let mut opened = d.open_with(&path, &opts)?; + if opened.metadata()?.is_dir() { + Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( + opened.into_std(), + ))) + } else if oflags.contains(OpenFlags::DIRECTORY) { + Ok(OpenResult::NotDir) + } else { + // FIXME cap-std needs a nonblocking open option so that files reads and writes + // are nonblocking. Instead we set it after opening here: + let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; + opened.set_fd_flags(set_fd_flags)?; + Ok(OpenResult::File(opened)) + } }) - .await??; + .await?; + + match opened { + OpenResult::Dir(dir) => Ok(table.push_dir(Dir::new(dir, d.perms, d.file_perms))?), + + OpenResult::File(file) => { + Ok(table.push_file(File::new(file, mask_file_perms(d.file_perms, flags)))?) + } - Ok(table.push_file(File::new(opened, mask_file_perms(d.file_perms, flags)))?) + OpenResult::NotDir => Err(ErrorCode::NotDirectory.into()), } } async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { let table = self.table_mut(); - spawn_blocking(|| { - if table.delete_file(fd).is_err() { - table.delete_dir(fd)?; - } - Ok(()) - }) - .await? + + // Table operations don't need to go in the background thread. + if table.delete_file(fd).is_err() { + table.delete_dir(fd)?; + } + + Ok(()) } async fn readlink_at( @@ -592,7 +606,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::READ) { return Err(ErrorCode::NotPermitted.into()); } - let link = spawn_blocking(|| d.dir.read_link(&path)).await??; + let link = d.block(|d| d.read_link(&path)).await?; Ok(link .into_os_string() .into_string() @@ -609,7 +623,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(spawn_blocking(|| d.dir.remove_dir(&path)).await??) + Ok(d.block(|d| d.remove_dir(&path)).await?) } async fn rename_at( @@ -628,8 +642,7 @@ impl filesystem::Host for T { if !new_dir.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - spawn_blocking(|| old_dir.dir.rename(&old_path, &new_dir.dir, &new_path)).await??; - Ok(()) + Ok(old_dir.block(|d| d.rename(&old_path, &new_dir.dir, &new_path)).await?) } async fn symlink_at( @@ -647,8 +660,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - spawn_blocking(|| d.dir.symlink(&src_path, &dest_path)).await??; - Ok(()) + Ok(d.block(|d| d.symlink(&src_path, &dest_path)).await?) } async fn unlink_file_at( @@ -663,8 +675,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - spawn_blocking(|| d.dir.remove_file_or_symlink(&path)).await??; - Ok(()) + Ok(d.block(|d| d.remove_file_or_symlink(&path)).await?) } async fn access_at( From d1e1e500c4a84fe08ea50869217763173aece805 Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 19 Jul 2023 12:27:36 -0700 Subject: [PATCH 086/118] Fix more lifetime issues with `block` --- .../wasi/src/preview2/preview2/filesystem.rs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 320c567ae95b..e0c9d809caee 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -149,7 +149,7 @@ impl filesystem::Host for T { if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; } - f.block(|f| f.set_len(size)).await?; + f.block(move |f| f.set_len(size)).await?; Ok(()) } @@ -201,11 +201,15 @@ impl filesystem::Host for T { return Err(ErrorCode::NotPermitted.into()); } - let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; - let (bytes_read, state) = crate::preview2::filesystem::read_result( - f.block(|f| f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset)) - .await, - )?; + let (mut buffer, r) = f + .block(move |f| { + let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; + let r = f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset); + (buffer, r) + }) + .await; + + let (bytes_read, state) = crate::preview2::filesystem::read_result(r)?; buffer.truncate( bytes_read @@ -232,7 +236,7 @@ impl filesystem::Host for T { } let bytes_written = f - .block(|f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) + .block(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) .await?; Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) @@ -360,7 +364,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - d.block(|d| d.create_dir(&path)).await?; + d.block(move |d| d.create_dir(&path)).await?; Ok(()) } @@ -397,9 +401,9 @@ impl filesystem::Host for T { } let meta = if symlink_follow(path_flags) { - d.block(|d| d.metadata(&path)).await? + d.block(move |d| d.metadata(&path)).await? } else { - d.block(|d| d.symlink_metadata(&path)).await? + d.block(move |d| d.symlink_metadata(&path)).await? }; Ok(descriptorstat_from(meta)) } @@ -422,7 +426,7 @@ impl filesystem::Host for T { let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; if symlink_follow(path_flags) { - d.block(|d| { + d.block(move |d| { d.set_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), @@ -431,7 +435,7 @@ impl filesystem::Host for T { }) .await?; } else { - d.block(|d| { + d.block(move |d| { d.set_symlink_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), @@ -464,8 +468,9 @@ impl filesystem::Host for T { if symlink_follow(old_path_flags) { return Err(ErrorCode::Invalid.into()); } + let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); old_dir - .block(|d| d.hard_link(&old_path, &new_dir.dir, &new_path)) + .block(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) .await?; Ok(()) } @@ -556,7 +561,7 @@ impl filesystem::Host for T { } let opened = d - .block::<_, std::io::Result>(|d| { + .block::<_, std::io::Result>(move |d| { let mut opened = d.open_with(&path, &opts)?; if opened.metadata()?.is_dir() { Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( @@ -606,7 +611,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::READ) { return Err(ErrorCode::NotPermitted.into()); } - let link = d.block(|d| d.read_link(&path)).await?; + let link = d.block(move |d| d.read_link(&path)).await?; Ok(link .into_os_string() .into_string() @@ -623,7 +628,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(|d| d.remove_dir(&path)).await?) + Ok(d.block(move |d| d.remove_dir(&path)).await?) } async fn rename_at( @@ -642,7 +647,10 @@ impl filesystem::Host for T { if !new_dir.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(old_dir.block(|d| d.rename(&old_path, &new_dir.dir, &new_path)).await?) + let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); + Ok(old_dir + .block(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) + .await?) } async fn symlink_at( @@ -660,7 +668,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(|d| d.symlink(&src_path, &dest_path)).await?) + Ok(d.block(move |d| d.symlink(&src_path, &dest_path)).await?) } async fn unlink_file_at( @@ -675,7 +683,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(|d| d.remove_file_or_symlink(&path)).await?) + Ok(d.block(move |d| d.remove_file_or_symlink(&path)).await?) } async fn access_at( From 73a5f4196cd6ae67d2dcd2feb5bb129ad783107d Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Wed, 19 Jul 2023 13:46:02 -0700 Subject: [PATCH 087/118] Finish filling out translation impl --- crates/wasi/src/preview2/mod.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 29155dbd570b..826d7567e96b 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -79,6 +79,38 @@ pub mod bindings { ErrorCode::Already => Self::Already, ErrorCode::BadDescriptor => Self::BadDescriptor, ErrorCode::Busy => Self::Busy, + ErrorCode::Deadlock => Self::Deadlock, + ErrorCode::Quota => Self::Quota, + ErrorCode::Exist => Self::Exist, + ErrorCode::FileTooLarge => Self::FileTooLarge, + ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, + ErrorCode::InProgress => Self::InProgress, + ErrorCode::Interrupted => Self::Interrupted, + ErrorCode::Invalid => Self::Invalid, + ErrorCode::Io => Self::Io, + ErrorCode::IsDirectory => Self::IsDirectory, + ErrorCode::Loop => Self::Loop, + ErrorCode::TooManyLinks => Self::TooManyLinks, + ErrorCode::MessageSize => Self::MessageSize, + ErrorCode::NameTooLong => Self::NameTooLong, + ErrorCode::NoDevice => Self::NoDevice, + ErrorCode::NoEntry => Self::NoEntry, + ErrorCode::NoLock => Self::NoLock, + ErrorCode::InsufficientMemory => Self::InsufficientMemory, + ErrorCode::InsufficientSpace => Self::InsufficientSpace, + ErrorCode::NotDirectory => Self::NotDirectory, + ErrorCode::NotEmpty => Self::NotEmpty, + ErrorCode::NotRecoverable => Self::NotRecoverable, + ErrorCode::Unsupported => Self::Unsupported, + ErrorCode::NoTty => Self::NoTty, + ErrorCode::NoSuchDevice => Self::NoSuchDevice, + ErrorCode::Overflow => Self::Overflow, + ErrorCode::NotPermitted => Self::NotPermitted, + ErrorCode::Pipe => Self::Pipe, + ErrorCode::ReadOnly => Self::ReadOnly, + ErrorCode::InvalidSeek => Self::InvalidSeek, + ErrorCode::TextFileBusy => Self::TextFileBusy, + ErrorCode::CrossDevice => Self::CrossDevice, } } } From 799249c2a36891e3b04cc004a75b76d1861a82ed Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 14:12:02 -0700 Subject: [PATCH 088/118] fix warnings --- crates/wasi/src/preview2/pipe.rs | 2 +- crates/wasi/src/preview2/stdio.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index f19480258e53..32c530332b63 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -407,7 +407,7 @@ pub struct ClosedOutputStream; #[async_trait::async_trait] impl HostOutputStream for ClosedOutputStream { - fn write(&mut self, buf: Bytes) -> Result<(usize, StreamState), Error> { + fn write(&mut self, _: Bytes) -> Result<(usize, StreamState), Error> { Ok((0, StreamState::Closed)) } diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index 86f3f1f60dea..b47ba772a1a4 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -1,7 +1,4 @@ -use anyhow::Error; -use bytes::Bytes; - -use crate::preview2::{pipe::AsyncWriteStream, HostInputStream, HostOutputStream, StreamState}; +use crate::preview2::pipe::AsyncWriteStream; #[cfg(unix)] mod unix; From b0629682e38d8ec6584fc41bf944a3a53668a3fa Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 14:17:09 -0700 Subject: [PATCH 089/118] we can likely eliminate block_in_place in the stdin implementations --- crates/wasi/src/preview2/stdio/unix.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index ac0bb2f33da8..d22aa26beca7 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -7,6 +7,12 @@ use tokio::io::unix::AsyncFd; // 1.70. when 1.71 is released, we can switch to using std here. use once_cell::sync::OnceCell as OnceLock; +// FIXME: we might be able to eliminate this, and block_in_place as well, +// if we write ready() with an impl Future that takes and releases a std::sync::mutex +// as part of every poll() invocation. It isnt critical that ready hold the +// lock for the duration of the polling - using stdin from multiple contexts +// is already bogus in terms of application functionality, we are just trying to +// make the implementation typecheck. // We use a tokio Mutex because, in ready(), the mutex needs to be held // across an await. use tokio::sync::Mutex; From fa92e35e1b403efc16a5f2e04f00fe8276751868 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 14:40:23 -0700 Subject: [PATCH 090/118] sync command uses sync filesystem, start of translation layer --- crates/wasi/src/preview2/command.rs | 4 +- crates/wasi/src/preview2/mod.rs | 45 - .../wasi/src/preview2/preview2/filesystem.rs | 2 + .../src/preview2/preview2/filesystem/sync.rs | 814 ++++++++++++++++++ 4 files changed, 818 insertions(+), 47 deletions(-) create mode 100644 crates/wasi/src/preview2/preview2/filesystem/sync.rs diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs index 155cb83afe05..fccfeaf52ed6 100644 --- a/crates/wasi/src/preview2/command.rs +++ b/crates/wasi/src/preview2/command.rs @@ -54,7 +54,7 @@ pub mod sync { "streams"::"stream-error": Error, }, with: { - "wasi:filesystem/filesystem": crate::preview2::bindings::filesystem::filesystem, + "wasi:filesystem/filesystem": crate::preview2::bindings::sync_io::filesystem::filesystem, "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, "wasi:poll/poll": crate::preview2::bindings::sync_io::poll::poll, "wasi:io/streams": crate::preview2::bindings::sync_io::io::streams, @@ -76,7 +76,7 @@ pub mod sync { crate::preview2::bindings::clocks::wall_clock::add_to_linker(l, |t| t)?; crate::preview2::bindings::clocks::monotonic_clock::add_to_linker(l, |t| t)?; crate::preview2::bindings::clocks::timezone::add_to_linker(l, |t| t)?; - crate::preview2::bindings::filesystem::filesystem::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sync_io::filesystem::filesystem::add_to_linker(l, |t| t)?; crate::preview2::bindings::sync_io::poll::poll::add_to_linker(l, |t| t)?; crate::preview2::bindings::sync_io::io::streams::add_to_linker(l, |t| t)?; crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?; diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 826d7567e96b..3c8171d8ec5b 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -70,51 +70,6 @@ pub mod bindings { } } - impl From for filesystem::filesystem::ErrorCode { - fn from(other: super::filesystem::filesystem::ErrorCode) -> Self { - use super::filesystem::filesystem::ErrorCode; - match other { - ErrorCode::Access => Self::Access, - ErrorCode::WouldBlock => Self::WouldBlock, - ErrorCode::Already => Self::Already, - ErrorCode::BadDescriptor => Self::BadDescriptor, - ErrorCode::Busy => Self::Busy, - ErrorCode::Deadlock => Self::Deadlock, - ErrorCode::Quota => Self::Quota, - ErrorCode::Exist => Self::Exist, - ErrorCode::FileTooLarge => Self::FileTooLarge, - ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, - ErrorCode::InProgress => Self::InProgress, - ErrorCode::Interrupted => Self::Interrupted, - ErrorCode::Invalid => Self::Invalid, - ErrorCode::Io => Self::Io, - ErrorCode::IsDirectory => Self::IsDirectory, - ErrorCode::Loop => Self::Loop, - ErrorCode::TooManyLinks => Self::TooManyLinks, - ErrorCode::MessageSize => Self::MessageSize, - ErrorCode::NameTooLong => Self::NameTooLong, - ErrorCode::NoDevice => Self::NoDevice, - ErrorCode::NoEntry => Self::NoEntry, - ErrorCode::NoLock => Self::NoLock, - ErrorCode::InsufficientMemory => Self::InsufficientMemory, - ErrorCode::InsufficientSpace => Self::InsufficientSpace, - ErrorCode::NotDirectory => Self::NotDirectory, - ErrorCode::NotEmpty => Self::NotEmpty, - ErrorCode::NotRecoverable => Self::NotRecoverable, - ErrorCode::Unsupported => Self::Unsupported, - ErrorCode::NoTty => Self::NoTty, - ErrorCode::NoSuchDevice => Self::NoSuchDevice, - ErrorCode::Overflow => Self::Overflow, - ErrorCode::NotPermitted => Self::NotPermitted, - ErrorCode::Pipe => Self::Pipe, - ErrorCode::ReadOnly => Self::ReadOnly, - ErrorCode::InvalidSeek => Self::InvalidSeek, - ErrorCode::TextFileBusy => Self::TextFileBusy, - ErrorCode::CrossDevice => Self::CrossDevice, - } - } - } - impl From for io::streams::Error { fn from(other: super::io::streams::Error) -> Self { match other.downcast() { diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index e0c9d809caee..72b3e6ea13e0 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -6,6 +6,8 @@ use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, Wa use filesystem::ErrorCode; +mod sync; + impl From for filesystem::Error { fn from(error: TableError) -> filesystem::Error { match error { diff --git a/crates/wasi/src/preview2/preview2/filesystem/sync.rs b/crates/wasi/src/preview2/preview2/filesystem/sync.rs new file mode 100644 index 000000000000..68ad5db5c9e8 --- /dev/null +++ b/crates/wasi/src/preview2/preview2/filesystem/sync.rs @@ -0,0 +1,814 @@ +use crate::preview2::bindings::filesystem::filesystem as async_filesystem; +use crate::preview2::bindings::sync_io::filesystem::filesystem as sync_filesystem; +use crate::preview2::block_on; + +impl sync_filesystem::Host for T { + fn advise( + &mut self, + fd: sync_filesystem::Descriptor, + offset: sync_filesystem::Filesize, + len: sync_filesystem::Filesize, + advice: sync_filesystem::Advice, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::advise(self, fd, offset, len, advice.into()).await + })?) + } + + fn sync_data(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::sync_data(self, fd).await + })?) + } + + fn get_flags( + &mut self, + fd: sync_filesystem::Descriptor, + ) -> Result { + Ok(block_on(async { async_filesystem::Host::get_flags(self, fd).await })?.into()) + } + + fn get_type( + &mut self, + fd: sync_filesystem::Descriptor, + ) -> Result { + Ok(block_on(async { async_filesystem::Host::get_type(self, fd).await })?.into()) + } + + fn set_size( + &mut self, + fd: sync_filesystem::Descriptor, + size: sync_filesystem::Filesize, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::set_size(self, fd, size).await + })?) + } + + /* + async fn set_times( + &mut self, + fd: filesystem::Descriptor, + atim: filesystem::NewTimestamp, + mtim: filesystem::NewTimestamp, + ) -> Result<(), filesystem::Error> { + use fs_set_times::SetTimes; + + let table = self.table(); + if table.is_file(fd) { + let f = table.get_file(fd)?; + if !f.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted.into()); + } + let atim = systemtimespec_from(atim)?; + let mtim = systemtimespec_from(mtim)?; + f.block(|f| f.set_times(atim, mtim)).await?; + Ok(()) + } else if table.is_dir(fd) { + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + let atim = systemtimespec_from(atim)?; + let mtim = systemtimespec_from(mtim)?; + d.block(|d| d.set_times(atim, mtim)).await?; + Ok(()) + } else { + Err(ErrorCode::BadDescriptor.into()) + } + } + + async fn read( + &mut self, + fd: filesystem::Descriptor, + len: filesystem::Filesize, + offset: filesystem::Filesize, + ) -> Result<(Vec, bool), filesystem::Error> { + use std::io::IoSliceMut; + use system_interface::fs::FileIoExt; + + let table = self.table(); + + let f = table.get_file(fd)?; + if !f.perms.contains(FilePerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + + let (mut buffer, r) = f + .block(move |f| { + let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; + let r = f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset); + (buffer, r) + }) + .await; + + let (bytes_read, state) = crate::preview2::filesystem::read_result(r)?; + + buffer.truncate( + bytes_read + .try_into() + .expect("bytes read into memory as u64 fits in usize"), + ); + + Ok((buffer, state.is_closed())) + } + + async fn write( + &mut self, + fd: filesystem::Descriptor, + buf: Vec, + offset: filesystem::Filesize, + ) -> Result { + use std::io::IoSlice; + use system_interface::fs::FileIoExt; + + let table = self.table(); + let f = table.get_file(fd)?; + if !f.perms.contains(FilePerms::WRITE) { + return Err(ErrorCode::NotPermitted.into()); + } + + let bytes_written = f + .block(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) + .await?; + + Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) + } + + async fn read_directory( + &mut self, + fd: filesystem::Descriptor, + ) -> Result { + use cap_fs_ext::{DirEntryExt, MetadataExt}; + + let table = self.table_mut(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + + enum ReaddirError { + Io(std::io::Error), + IllegalSequence, + } + impl From for ReaddirError { + fn from(e: std::io::Error) -> ReaddirError { + ReaddirError::Io(e) + } + } + + let entries = d + .block(|d| { + // Both `entries` and `full_metadata` perform syscalls, which is why they are done + // within this `block` call, rather than delay calculating the full metadata + // for entries when they're demanded later in the iterator chain. + Ok::<_, std::io::Error>( + d.entries()? + .map(|entry| { + let entry = entry?; + let meta = entry.full_metadata()?; + let inode = Some(meta.ino()); + let type_ = descriptortype_from(meta.file_type()); + let name = entry + .file_name() + .into_string() + .map_err(|_| ReaddirError::IllegalSequence)?; + Ok(filesystem::DirectoryEntry { inode, type_, name }) + }) + .collect::>>(), + ) + }) + .await? + .into_iter(); + + // On windows, filter out files like `C:\DumpStack.log.tmp` which we + // can't get full metadata for. + #[cfg(windows)] + let entries = entries.filter(|entry| { + use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION}; + if let Err(ReaddirError::Io(err)) = entry { + if err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) + || err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) + { + return false; + } + } + true + }); + let entries = entries.map(|r| match r { + Ok(r) => Ok(r), + Err(ReaddirError::Io(e)) => Err(filesystem::Error::from(e)), + Err(ReaddirError::IllegalSequence) => Err(ErrorCode::IllegalByteSequence.into()), + }); + Ok(table.push_readdir(ReaddirIterator::new(entries))?) + } + + async fn read_directory_entry( + &mut self, + stream: filesystem::DirectoryEntryStream, + ) -> Result, filesystem::Error> { + let table = self.table(); + let readdir = table.get_readdir(stream)?; + readdir.next() + } + + async fn drop_directory_entry_stream( + &mut self, + stream: filesystem::DirectoryEntryStream, + ) -> anyhow::Result<()> { + self.table_mut().delete_readdir(stream)?; + Ok(()) + } + + async fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + let table = self.table(); + if table.is_file(fd) { + let f = table.get_file(fd)?; + match f.block(|f| f.sync_all()).await { + Ok(()) => Ok(()), + // On windows, `sync_data` uses `FileFlushBuffers` which fails with + // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore + // this error, for POSIX compatibility. + #[cfg(windows)] + Err(e) + if e.raw_os_error() + == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) => + { + Ok(()) + } + Err(e) => Err(e.into()), + } + } else if table.is_dir(fd) { + let d = table.get_dir(fd)?; + d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_all()?)) + .await + } else { + Err(ErrorCode::BadDescriptor.into()) + } + } + + async fn create_directory_at( + &mut self, + fd: filesystem::Descriptor, + path: String, + ) -> Result<(), filesystem::Error> { + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + d.block(move |d| d.create_dir(&path)).await?; + Ok(()) + } + + async fn stat( + &mut self, + fd: filesystem::Descriptor, + ) -> Result { + let table = self.table(); + if table.is_file(fd) { + let f = table.get_file(fd)?; + // No permissions check on stat: if opened, allowed to stat it + let meta = f.block(|f| f.metadata()).await?; + Ok(descriptorstat_from(meta)) + } else if table.is_dir(fd) { + let d = table.get_dir(fd)?; + // No permissions check on stat: if opened, allowed to stat it + let meta = d.block(|d| d.dir_metadata()).await?; + Ok(descriptorstat_from(meta)) + } else { + Err(ErrorCode::BadDescriptor.into()) + } + } + + async fn stat_at( + &mut self, + fd: filesystem::Descriptor, + path_flags: filesystem::PathFlags, + path: String, + ) -> Result { + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + + let meta = if symlink_follow(path_flags) { + d.block(move |d| d.metadata(&path)).await? + } else { + d.block(move |d| d.symlink_metadata(&path)).await? + }; + Ok(descriptorstat_from(meta)) + } + + async fn set_times_at( + &mut self, + fd: filesystem::Descriptor, + path_flags: filesystem::PathFlags, + path: String, + atim: filesystem::NewTimestamp, + mtim: filesystem::NewTimestamp, + ) -> Result<(), filesystem::Error> { + use cap_fs_ext::DirExt; + + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + let atim = systemtimespec_from(atim)?; + let mtim = systemtimespec_from(mtim)?; + if symlink_follow(path_flags) { + d.block(move |d| { + d.set_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + }) + .await?; + } else { + d.block(move |d| { + d.set_symlink_times( + &path, + atim.map(cap_fs_ext::SystemTimeSpec::from_std), + mtim.map(cap_fs_ext::SystemTimeSpec::from_std), + ) + }) + .await?; + } + Ok(()) + } + + async fn link_at( + &mut self, + fd: filesystem::Descriptor, + // TODO delete the path flags from this function + old_path_flags: filesystem::PathFlags, + old_path: String, + new_descriptor: filesystem::Descriptor, + new_path: String, + ) -> Result<(), filesystem::Error> { + let table = self.table(); + let old_dir = table.get_dir(fd)?; + if !old_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + let new_dir = table.get_dir(new_descriptor)?; + if !new_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + if symlink_follow(old_path_flags) { + return Err(ErrorCode::Invalid.into()); + } + let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); + old_dir + .block(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) + .await?; + Ok(()) + } + + async fn open_at( + &mut self, + fd: filesystem::Descriptor, + path_flags: filesystem::PathFlags, + path: String, + oflags: filesystem::OpenFlags, + flags: filesystem::DescriptorFlags, + // TODO: These are the permissions to use when creating a new file. + // Not implemented yet. + _mode: filesystem::Modes, + ) -> Result { + use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt}; + use filesystem::{DescriptorFlags, OpenFlags}; + use system_interface::fs::{FdFlags, GetSetFdFlags}; + + let table = self.table_mut(); + if table.is_file(fd) { + Err(ErrorCode::NotDirectory)?; + } + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::READ) { + Err(ErrorCode::NotPermitted)?; + } + + if !d.perms.contains(DirPerms::MUTATE) { + if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) { + Err(ErrorCode::NotPermitted)?; + } + if flags.contains(DescriptorFlags::WRITE) { + Err(ErrorCode::NotPermitted)?; + } + } + + let mut opts = cap_std::fs::OpenOptions::new(); + opts.maybe_dir(true); + + if oflags.contains(OpenFlags::CREATE | OpenFlags::EXCLUSIVE) { + opts.create_new(true); + opts.write(true); + } else if oflags.contains(OpenFlags::CREATE) { + opts.create(true); + opts.write(true); + } + if oflags.contains(OpenFlags::TRUNCATE) { + opts.truncate(true); + } + if flags.contains(DescriptorFlags::READ) { + opts.read(true); + } + if flags.contains(DescriptorFlags::WRITE) { + opts.write(true); + } else { + // If not opened write, open read. This way the OS lets us open + // the file, but we can use perms to reject use of the file later. + opts.read(true); + } + if symlink_follow(path_flags) { + opts.follow(FollowSymlinks::Yes); + } else { + opts.follow(FollowSymlinks::No); + } + + // These flags are not yet supported in cap-std: + if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC) + | flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC) + | flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC) + { + Err(ErrorCode::Unsupported)?; + } + + if oflags.contains(OpenFlags::DIRECTORY) { + if oflags.contains(OpenFlags::CREATE) + || oflags.contains(OpenFlags::EXCLUSIVE) + || oflags.contains(OpenFlags::TRUNCATE) + { + Err(ErrorCode::Invalid)?; + } + } + + enum OpenResult { + Dir(cap_std::fs::Dir), + File(cap_std::fs::File), + NotDir, + } + + let opened = d + .block::<_, std::io::Result>(move |d| { + let mut opened = d.open_with(&path, &opts)?; + if opened.metadata()?.is_dir() { + Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( + opened.into_std(), + ))) + } else if oflags.contains(OpenFlags::DIRECTORY) { + Ok(OpenResult::NotDir) + } else { + // FIXME cap-std needs a nonblocking open option so that files reads and writes + // are nonblocking. Instead we set it after opening here: + let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; + opened.set_fd_flags(set_fd_flags)?; + Ok(OpenResult::File(opened)) + } + }) + .await?; + + match opened { + OpenResult::Dir(dir) => Ok(table.push_dir(Dir::new(dir, d.perms, d.file_perms))?), + + OpenResult::File(file) => { + Ok(table.push_file(File::new(file, mask_file_perms(d.file_perms, flags)))?) + } + + OpenResult::NotDir => Err(ErrorCode::NotDirectory.into()), + } + } + + async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { + let table = self.table_mut(); + + // Table operations don't need to go in the background thread. + if table.delete_file(fd).is_err() { + table.delete_dir(fd)?; + } + + Ok(()) + } + + async fn readlink_at( + &mut self, + fd: filesystem::Descriptor, + path: String, + ) -> Result { + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::READ) { + return Err(ErrorCode::NotPermitted.into()); + } + let link = d.block(move |d| d.read_link(&path)).await?; + Ok(link + .into_os_string() + .into_string() + .map_err(|_| ErrorCode::IllegalByteSequence)?) + } + + async fn remove_directory_at( + &mut self, + fd: filesystem::Descriptor, + path: String, + ) -> Result<(), filesystem::Error> { + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + Ok(d.block(move |d| d.remove_dir(&path)).await?) + } + + async fn rename_at( + &mut self, + fd: filesystem::Descriptor, + old_path: String, + new_fd: filesystem::Descriptor, + new_path: String, + ) -> Result<(), filesystem::Error> { + let table = self.table(); + let old_dir = table.get_dir(fd)?; + if !old_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + let new_dir = table.get_dir(new_fd)?; + if !new_dir.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); + Ok(old_dir + .block(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) + .await?) + } + + async fn symlink_at( + &mut self, + fd: filesystem::Descriptor, + src_path: String, + dest_path: String, + ) -> Result<(), filesystem::Error> { + // On windows, Dir.symlink is provided by DirExt + #[cfg(windows)] + use cap_fs_ext::DirExt; + + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + Ok(d.block(move |d| d.symlink(&src_path, &dest_path)).await?) + } + + async fn unlink_file_at( + &mut self, + fd: filesystem::Descriptor, + path: String, + ) -> Result<(), filesystem::Error> { + use cap_fs_ext::DirExt; + + let table = self.table(); + let d = table.get_dir(fd)?; + if !d.perms.contains(DirPerms::MUTATE) { + return Err(ErrorCode::NotPermitted.into()); + } + Ok(d.block(move |d| d.remove_file_or_symlink(&path)).await?) + } + + async fn access_at( + &mut self, + _fd: filesystem::Descriptor, + _path_flags: filesystem::PathFlags, + _path: String, + _access: filesystem::AccessType, + ) -> Result<(), filesystem::Error> { + todo!("filesystem access_at is not implemented") + } + + async fn change_file_permissions_at( + &mut self, + _fd: filesystem::Descriptor, + _path_flags: filesystem::PathFlags, + _path: String, + _mode: filesystem::Modes, + ) -> Result<(), filesystem::Error> { + todo!("filesystem change_file_permissions_at is not implemented") + } + + async fn change_directory_permissions_at( + &mut self, + _fd: filesystem::Descriptor, + _path_flags: filesystem::PathFlags, + _path: String, + _mode: filesystem::Modes, + ) -> Result<(), filesystem::Error> { + todo!("filesystem change_directory_permissions_at is not implemented") + } + + async fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + todo!("filesystem lock_shared is not implemented") + } + + async fn lock_exclusive( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { + todo!("filesystem lock_exclusive is not implemented") + } + + async fn try_lock_shared( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { + todo!("filesystem try_lock_shared is not implemented") + } + + async fn try_lock_exclusive( + &mut self, + _fd: filesystem::Descriptor, + ) -> Result<(), filesystem::Error> { + todo!("filesystem try_lock_exclusive is not implemented") + } + + async fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { + todo!("filesystem unlock is not implemented") + } + + async fn read_via_stream( + &mut self, + fd: filesystem::Descriptor, + offset: filesystem::Filesize, + ) -> Result { + // Trap if fd lookup fails: + let f = self.table().get_file(fd)?; + + if !f.perms.contains(FilePerms::READ) { + Err(filesystem::ErrorCode::BadDescriptor)?; + } + // Duplicate the file descriptor so that we get an indepenent lifetime. + let clone = std::sync::Arc::clone(&f.file); + + // Create a stream view for it. + let reader = crate::preview2::filesystem::FileInputStream::new(clone, offset); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table_mut().push_input_stream(Box::new(reader))?; + + Ok(index) + } + + async fn write_via_stream( + &mut self, + fd: filesystem::Descriptor, + offset: filesystem::Filesize, + ) -> Result { + // Trap if fd lookup fails: + let f = self.table().get_file(fd)?; + + if !f.perms.contains(FilePerms::WRITE) { + Err(filesystem::ErrorCode::BadDescriptor)?; + } + + // Duplicate the file descriptor so that we get an indepenent lifetime. + let clone = std::sync::Arc::clone(&f.file); + + // Create a stream view for it. + let writer = crate::preview2::filesystem::FileOutputStream::new(clone, offset); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table_mut().push_output_stream(Box::new(writer))?; + + Ok(index) + } + + async fn append_via_stream( + &mut self, + fd: filesystem::Descriptor, + ) -> Result { + // Trap if fd lookup fails: + let f = self.table().get_file(fd)?; + + if !f.perms.contains(FilePerms::WRITE) { + Err(filesystem::ErrorCode::BadDescriptor)?; + } + // Duplicate the file descriptor so that we get an indepenent lifetime. + let clone = std::sync::Arc::clone(&f.file); + + // Create a stream view for it. + let appender = crate::preview2::filesystem::FileAppendStream::new(clone); + + // Insert the stream view into the table. Trap if the table is full. + let index = self.table_mut().push_output_stream(Box::new(appender))?; + + Ok(index) + } + */ +} + +impl From for sync_filesystem::ErrorCode { + fn from(other: async_filesystem::ErrorCode) -> Self { + use async_filesystem::ErrorCode; + match other { + ErrorCode::Access => Self::Access, + ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::Already => Self::Already, + ErrorCode::BadDescriptor => Self::BadDescriptor, + ErrorCode::Busy => Self::Busy, + ErrorCode::Deadlock => Self::Deadlock, + ErrorCode::Quota => Self::Quota, + ErrorCode::Exist => Self::Exist, + ErrorCode::FileTooLarge => Self::FileTooLarge, + ErrorCode::IllegalByteSequence => Self::IllegalByteSequence, + ErrorCode::InProgress => Self::InProgress, + ErrorCode::Interrupted => Self::Interrupted, + ErrorCode::Invalid => Self::Invalid, + ErrorCode::Io => Self::Io, + ErrorCode::IsDirectory => Self::IsDirectory, + ErrorCode::Loop => Self::Loop, + ErrorCode::TooManyLinks => Self::TooManyLinks, + ErrorCode::MessageSize => Self::MessageSize, + ErrorCode::NameTooLong => Self::NameTooLong, + ErrorCode::NoDevice => Self::NoDevice, + ErrorCode::NoEntry => Self::NoEntry, + ErrorCode::NoLock => Self::NoLock, + ErrorCode::InsufficientMemory => Self::InsufficientMemory, + ErrorCode::InsufficientSpace => Self::InsufficientSpace, + ErrorCode::NotDirectory => Self::NotDirectory, + ErrorCode::NotEmpty => Self::NotEmpty, + ErrorCode::NotRecoverable => Self::NotRecoverable, + ErrorCode::Unsupported => Self::Unsupported, + ErrorCode::NoTty => Self::NoTty, + ErrorCode::NoSuchDevice => Self::NoSuchDevice, + ErrorCode::Overflow => Self::Overflow, + ErrorCode::NotPermitted => Self::NotPermitted, + ErrorCode::Pipe => Self::Pipe, + ErrorCode::ReadOnly => Self::ReadOnly, + ErrorCode::InvalidSeek => Self::InvalidSeek, + ErrorCode::TextFileBusy => Self::TextFileBusy, + ErrorCode::CrossDevice => Self::CrossDevice, + } + } +} + +impl From for sync_filesystem::Error { + fn from(other: async_filesystem::Error) -> Self { + match other.downcast() { + Ok(errorcode) => Self::from(sync_filesystem::ErrorCode::from(errorcode)), + Err(other) => Self::trap(other), + } + } +} + +impl From for async_filesystem::Advice { + fn from(other: sync_filesystem::Advice) -> Self { + use sync_filesystem::Advice; + match other { + Advice::Normal => Self::Normal, + Advice::Sequential => Self::Sequential, + Advice::Random => Self::Random, + Advice::WillNeed => Self::WillNeed, + Advice::DontNeed => Self::DontNeed, + Advice::NoReuse => Self::NoReuse, + } + } +} + +impl From for sync_filesystem::DescriptorFlags { + fn from(other: async_filesystem::DescriptorFlags) -> Self { + let mut out = Self::empty(); + if other.contains(async_filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if other.contains(async_filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if other.contains(async_filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if other.contains(async_filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if other.contains(async_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + out + } +} + +impl From for sync_filesystem::DescriptorType { + fn from(other: async_filesystem::DescriptorType) -> Self { + use async_filesystem::DescriptorType; + match other { + DescriptorType::RegularFile => Self::RegularFile, + DescriptorType::Directory => Self::Directory, + DescriptorType::BlockDevice => Self::BlockDevice, + DescriptorType::CharacterDevice => Self::CharacterDevice, + DescriptorType::Fifo => Self::Fifo, + DescriptorType::Socket => Self::Socket, + } + } +} From d0925d4eedfb99c0127b4db3723667b764b3fd0b Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 15:03:47 -0700 Subject: [PATCH 091/118] symc filesystem: all the trait boilerplate is in place just need to finish the from impl boilerplate --- .../src/preview2/preview2/filesystem/sync.rs | 856 ++++++------------ 1 file changed, 292 insertions(+), 564 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/filesystem/sync.rs b/crates/wasi/src/preview2/preview2/filesystem/sync.rs index 68ad5db5c9e8..bf44e57ff5f7 100644 --- a/crates/wasi/src/preview2/preview2/filesystem/sync.rs +++ b/crates/wasi/src/preview2/preview2/filesystem/sync.rs @@ -1,5 +1,6 @@ use crate::preview2::bindings::filesystem::filesystem as async_filesystem; use crate::preview2::bindings::sync_io::filesystem::filesystem as sync_filesystem; +use crate::preview2::bindings::sync_io::io::streams; use crate::preview2::block_on; impl sync_filesystem::Host for T { @@ -45,668 +46,348 @@ impl sync_filesystem::Host for T { })?) } - /* - async fn set_times( + fn set_times( &mut self, - fd: filesystem::Descriptor, - atim: filesystem::NewTimestamp, - mtim: filesystem::NewTimestamp, - ) -> Result<(), filesystem::Error> { - use fs_set_times::SetTimes; - - let table = self.table(); - if table.is_file(fd) { - let f = table.get_file(fd)?; - if !f.perms.contains(FilePerms::WRITE) { - return Err(ErrorCode::NotPermitted.into()); - } - let atim = systemtimespec_from(atim)?; - let mtim = systemtimespec_from(mtim)?; - f.block(|f| f.set_times(atim, mtim)).await?; - Ok(()) - } else if table.is_dir(fd) { - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - let atim = systemtimespec_from(atim)?; - let mtim = systemtimespec_from(mtim)?; - d.block(|d| d.set_times(atim, mtim)).await?; - Ok(()) - } else { - Err(ErrorCode::BadDescriptor.into()) - } + fd: sync_filesystem::Descriptor, + atim: sync_filesystem::NewTimestamp, + mtim: sync_filesystem::NewTimestamp, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::set_times(self, fd, atim.into(), mtim.into()).await + })?) } - async fn read( + fn read( &mut self, - fd: filesystem::Descriptor, - len: filesystem::Filesize, - offset: filesystem::Filesize, - ) -> Result<(Vec, bool), filesystem::Error> { - use std::io::IoSliceMut; - use system_interface::fs::FileIoExt; - - let table = self.table(); - - let f = table.get_file(fd)?; - if !f.perms.contains(FilePerms::READ) { - return Err(ErrorCode::NotPermitted.into()); - } - - let (mut buffer, r) = f - .block(move |f| { - let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; - let r = f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset); - (buffer, r) - }) - .await; - - let (bytes_read, state) = crate::preview2::filesystem::read_result(r)?; - - buffer.truncate( - bytes_read - .try_into() - .expect("bytes read into memory as u64 fits in usize"), - ); - - Ok((buffer, state.is_closed())) + fd: sync_filesystem::Descriptor, + len: sync_filesystem::Filesize, + offset: sync_filesystem::Filesize, + ) -> Result<(Vec, bool), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::read(self, fd, len, offset).await + })?) } - async fn write( + fn write( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, buf: Vec, - offset: filesystem::Filesize, - ) -> Result { - use std::io::IoSlice; - use system_interface::fs::FileIoExt; - - let table = self.table(); - let f = table.get_file(fd)?; - if !f.perms.contains(FilePerms::WRITE) { - return Err(ErrorCode::NotPermitted.into()); - } - - let bytes_written = f - .block(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) - .await?; - - Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) + offset: sync_filesystem::Filesize, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::write(self, fd, buf, offset).await + })?) } - async fn read_directory( + fn read_directory( &mut self, - fd: filesystem::Descriptor, - ) -> Result { - use cap_fs_ext::{DirEntryExt, MetadataExt}; - - let table = self.table_mut(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::READ) { - return Err(ErrorCode::NotPermitted.into()); - } - - enum ReaddirError { - Io(std::io::Error), - IllegalSequence, - } - impl From for ReaddirError { - fn from(e: std::io::Error) -> ReaddirError { - ReaddirError::Io(e) - } - } + fd: sync_filesystem::Descriptor, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::read_directory(self, fd).await + })?) + } - let entries = d - .block(|d| { - // Both `entries` and `full_metadata` perform syscalls, which is why they are done - // within this `block` call, rather than delay calculating the full metadata - // for entries when they're demanded later in the iterator chain. - Ok::<_, std::io::Error>( - d.entries()? - .map(|entry| { - let entry = entry?; - let meta = entry.full_metadata()?; - let inode = Some(meta.ino()); - let type_ = descriptortype_from(meta.file_type()); - let name = entry - .file_name() - .into_string() - .map_err(|_| ReaddirError::IllegalSequence)?; - Ok(filesystem::DirectoryEntry { inode, type_, name }) - }) - .collect::>>(), - ) - }) - .await? - .into_iter(); - - // On windows, filter out files like `C:\DumpStack.log.tmp` which we - // can't get full metadata for. - #[cfg(windows)] - let entries = entries.filter(|entry| { - use windows_sys::Win32::Foundation::{ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION}; - if let Err(ReaddirError::Io(err)) = entry { - if err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) - || err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) - { - return false; - } - } - true - }); - let entries = entries.map(|r| match r { - Ok(r) => Ok(r), - Err(ReaddirError::Io(e)) => Err(filesystem::Error::from(e)), - Err(ReaddirError::IllegalSequence) => Err(ErrorCode::IllegalByteSequence.into()), - }); - Ok(table.push_readdir(ReaddirIterator::new(entries))?) - } - - async fn read_directory_entry( + fn read_directory_entry( &mut self, - stream: filesystem::DirectoryEntryStream, - ) -> Result, filesystem::Error> { - let table = self.table(); - let readdir = table.get_readdir(stream)?; - readdir.next() + stream: sync_filesystem::DirectoryEntryStream, + ) -> Result, sync_filesystem::Error> { + Ok( + block_on(async { async_filesystem::Host::read_directory_entry(self, stream).await })? + .map(|e| e.into()), + ) } - async fn drop_directory_entry_stream( + fn drop_directory_entry_stream( &mut self, - stream: filesystem::DirectoryEntryStream, + stream: sync_filesystem::DirectoryEntryStream, ) -> anyhow::Result<()> { - self.table_mut().delete_readdir(stream)?; - Ok(()) - } - - async fn sync(&mut self, fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - let table = self.table(); - if table.is_file(fd) { - let f = table.get_file(fd)?; - match f.block(|f| f.sync_all()).await { - Ok(()) => Ok(()), - // On windows, `sync_data` uses `FileFlushBuffers` which fails with - // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore - // this error, for POSIX compatibility. - #[cfg(windows)] - Err(e) - if e.raw_os_error() - == Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as _) => - { - Ok(()) - } - Err(e) => Err(e.into()), - } - } else if table.is_dir(fd) { - let d = table.get_dir(fd)?; - d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_all()?)) - .await - } else { - Err(ErrorCode::BadDescriptor.into()) - } + Ok(block_on(async { + async_filesystem::Host::drop_directory_entry_stream(self, stream).await + })?) + } + + fn sync(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::sync(self, fd).await + })?) } - async fn create_directory_at( + fn create_directory_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, path: String, - ) -> Result<(), filesystem::Error> { - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - d.block(move |d| d.create_dir(&path)).await?; - Ok(()) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::create_directory_at(self, fd, path).await + })?) } - async fn stat( + fn stat( &mut self, - fd: filesystem::Descriptor, - ) -> Result { - let table = self.table(); - if table.is_file(fd) { - let f = table.get_file(fd)?; - // No permissions check on stat: if opened, allowed to stat it - let meta = f.block(|f| f.metadata()).await?; - Ok(descriptorstat_from(meta)) - } else if table.is_dir(fd) { - let d = table.get_dir(fd)?; - // No permissions check on stat: if opened, allowed to stat it - let meta = d.block(|d| d.dir_metadata()).await?; - Ok(descriptorstat_from(meta)) - } else { - Err(ErrorCode::BadDescriptor.into()) - } + fd: sync_filesystem::Descriptor, + ) -> Result { + Ok(block_on(async { async_filesystem::Host::stat(self, fd).await })?.into()) } - async fn stat_at( + fn stat_at( &mut self, - fd: filesystem::Descriptor, - path_flags: filesystem::PathFlags, + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, path: String, - ) -> Result { - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::READ) { - return Err(ErrorCode::NotPermitted.into()); - } - - let meta = if symlink_follow(path_flags) { - d.block(move |d| d.metadata(&path)).await? - } else { - d.block(move |d| d.symlink_metadata(&path)).await? - }; - Ok(descriptorstat_from(meta)) + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::stat_at(self, fd, path_flags.into(), path).await + })? + .into()) } - async fn set_times_at( + fn set_times_at( &mut self, - fd: filesystem::Descriptor, - path_flags: filesystem::PathFlags, + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, path: String, - atim: filesystem::NewTimestamp, - mtim: filesystem::NewTimestamp, - ) -> Result<(), filesystem::Error> { - use cap_fs_ext::DirExt; - - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - let atim = systemtimespec_from(atim)?; - let mtim = systemtimespec_from(mtim)?; - if symlink_follow(path_flags) { - d.block(move |d| { - d.set_times( - &path, - atim.map(cap_fs_ext::SystemTimeSpec::from_std), - mtim.map(cap_fs_ext::SystemTimeSpec::from_std), - ) - }) - .await?; - } else { - d.block(move |d| { - d.set_symlink_times( - &path, - atim.map(cap_fs_ext::SystemTimeSpec::from_std), - mtim.map(cap_fs_ext::SystemTimeSpec::from_std), - ) - }) - .await?; - } - Ok(()) + atim: sync_filesystem::NewTimestamp, + mtim: sync_filesystem::NewTimestamp, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::set_times_at( + self, + fd, + path_flags.into(), + path, + atim.into(), + mtim.into(), + ) + .await + })?) } - async fn link_at( + fn link_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, // TODO delete the path flags from this function - old_path_flags: filesystem::PathFlags, + old_path_flags: sync_filesystem::PathFlags, old_path: String, - new_descriptor: filesystem::Descriptor, + new_descriptor: sync_filesystem::Descriptor, new_path: String, - ) -> Result<(), filesystem::Error> { - let table = self.table(); - let old_dir = table.get_dir(fd)?; - if !old_dir.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - let new_dir = table.get_dir(new_descriptor)?; - if !new_dir.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - if symlink_follow(old_path_flags) { - return Err(ErrorCode::Invalid.into()); - } - let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); - old_dir - .block(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) - .await?; - Ok(()) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::link_at( + self, + fd, + old_path_flags.into(), + old_path, + new_descriptor, + new_path, + ) + .await + })?) } - async fn open_at( + fn open_at( &mut self, - fd: filesystem::Descriptor, - path_flags: filesystem::PathFlags, + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, path: String, - oflags: filesystem::OpenFlags, - flags: filesystem::DescriptorFlags, - // TODO: These are the permissions to use when creating a new file. - // Not implemented yet. - _mode: filesystem::Modes, - ) -> Result { - use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt}; - use filesystem::{DescriptorFlags, OpenFlags}; - use system_interface::fs::{FdFlags, GetSetFdFlags}; - - let table = self.table_mut(); - if table.is_file(fd) { - Err(ErrorCode::NotDirectory)?; - } - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::READ) { - Err(ErrorCode::NotPermitted)?; - } - - if !d.perms.contains(DirPerms::MUTATE) { - if oflags.contains(OpenFlags::CREATE) || oflags.contains(OpenFlags::TRUNCATE) { - Err(ErrorCode::NotPermitted)?; - } - if flags.contains(DescriptorFlags::WRITE) { - Err(ErrorCode::NotPermitted)?; - } - } - - let mut opts = cap_std::fs::OpenOptions::new(); - opts.maybe_dir(true); - - if oflags.contains(OpenFlags::CREATE | OpenFlags::EXCLUSIVE) { - opts.create_new(true); - opts.write(true); - } else if oflags.contains(OpenFlags::CREATE) { - opts.create(true); - opts.write(true); - } - if oflags.contains(OpenFlags::TRUNCATE) { - opts.truncate(true); - } - if flags.contains(DescriptorFlags::READ) { - opts.read(true); - } - if flags.contains(DescriptorFlags::WRITE) { - opts.write(true); - } else { - // If not opened write, open read. This way the OS lets us open - // the file, but we can use perms to reject use of the file later. - opts.read(true); - } - if symlink_follow(path_flags) { - opts.follow(FollowSymlinks::Yes); - } else { - opts.follow(FollowSymlinks::No); - } - - // These flags are not yet supported in cap-std: - if flags.contains(DescriptorFlags::FILE_INTEGRITY_SYNC) - | flags.contains(DescriptorFlags::DATA_INTEGRITY_SYNC) - | flags.contains(DescriptorFlags::REQUESTED_WRITE_SYNC) - { - Err(ErrorCode::Unsupported)?; - } - - if oflags.contains(OpenFlags::DIRECTORY) { - if oflags.contains(OpenFlags::CREATE) - || oflags.contains(OpenFlags::EXCLUSIVE) - || oflags.contains(OpenFlags::TRUNCATE) - { - Err(ErrorCode::Invalid)?; - } - } - - enum OpenResult { - Dir(cap_std::fs::Dir), - File(cap_std::fs::File), - NotDir, - } - - let opened = d - .block::<_, std::io::Result>(move |d| { - let mut opened = d.open_with(&path, &opts)?; - if opened.metadata()?.is_dir() { - Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( - opened.into_std(), - ))) - } else if oflags.contains(OpenFlags::DIRECTORY) { - Ok(OpenResult::NotDir) - } else { - // FIXME cap-std needs a nonblocking open option so that files reads and writes - // are nonblocking. Instead we set it after opening here: - let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?; - opened.set_fd_flags(set_fd_flags)?; - Ok(OpenResult::File(opened)) - } - }) - .await?; - - match opened { - OpenResult::Dir(dir) => Ok(table.push_dir(Dir::new(dir, d.perms, d.file_perms))?), - - OpenResult::File(file) => { - Ok(table.push_file(File::new(file, mask_file_perms(d.file_perms, flags)))?) - } - - OpenResult::NotDir => Err(ErrorCode::NotDirectory.into()), - } + oflags: sync_filesystem::OpenFlags, + flags: sync_filesystem::DescriptorFlags, + mode: sync_filesystem::Modes, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::open_at( + self, + fd, + path_flags.into(), + path, + oflags.into(), + flags.into(), + mode.into(), + ) + .await + })?) } - async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { - let table = self.table_mut(); - - // Table operations don't need to go in the background thread. - if table.delete_file(fd).is_err() { - table.delete_dir(fd)?; - } - - Ok(()) + fn drop_descriptor(&mut self, fd: sync_filesystem::Descriptor) -> anyhow::Result<()> { + Ok(block_on(async { + async_filesystem::Host::drop_descriptor(self, fd).await + })?) } - async fn readlink_at( + fn readlink_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, path: String, - ) -> Result { - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::READ) { - return Err(ErrorCode::NotPermitted.into()); - } - let link = d.block(move |d| d.read_link(&path)).await?; - Ok(link - .into_os_string() - .into_string() - .map_err(|_| ErrorCode::IllegalByteSequence)?) + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::readlink_at(self, fd, path).await + })?) } - async fn remove_directory_at( + fn remove_directory_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, path: String, - ) -> Result<(), filesystem::Error> { - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - Ok(d.block(move |d| d.remove_dir(&path)).await?) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::remove_directory_at(self, fd, path).await + })?) } - async fn rename_at( + fn rename_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, old_path: String, - new_fd: filesystem::Descriptor, + new_fd: sync_filesystem::Descriptor, new_path: String, - ) -> Result<(), filesystem::Error> { - let table = self.table(); - let old_dir = table.get_dir(fd)?; - if !old_dir.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - let new_dir = table.get_dir(new_fd)?; - if !new_dir.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); - Ok(old_dir - .block(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) - .await?) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::rename_at(self, fd, old_path, new_fd, new_path).await + })?) } - async fn symlink_at( + fn symlink_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, src_path: String, dest_path: String, - ) -> Result<(), filesystem::Error> { - // On windows, Dir.symlink is provided by DirExt - #[cfg(windows)] - use cap_fs_ext::DirExt; - - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - Ok(d.block(move |d| d.symlink(&src_path, &dest_path)).await?) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::symlink_at(self, fd, src_path, dest_path).await + })?) } - async fn unlink_file_at( + fn unlink_file_at( &mut self, - fd: filesystem::Descriptor, + fd: sync_filesystem::Descriptor, path: String, - ) -> Result<(), filesystem::Error> { - use cap_fs_ext::DirExt; - - let table = self.table(); - let d = table.get_dir(fd)?; - if !d.perms.contains(DirPerms::MUTATE) { - return Err(ErrorCode::NotPermitted.into()); - } - Ok(d.block(move |d| d.remove_file_or_symlink(&path)).await?) + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::unlink_file_at(self, fd, path).await + })?) } - async fn access_at( + fn access_at( &mut self, - _fd: filesystem::Descriptor, - _path_flags: filesystem::PathFlags, - _path: String, - _access: filesystem::AccessType, - ) -> Result<(), filesystem::Error> { - todo!("filesystem access_at is not implemented") + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, + path: String, + access: sync_filesystem::AccessType, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::access_at(self, fd, path_flags.into(), path, access.into()) + .await + })?) } - async fn change_file_permissions_at( + fn change_file_permissions_at( &mut self, - _fd: filesystem::Descriptor, - _path_flags: filesystem::PathFlags, - _path: String, - _mode: filesystem::Modes, - ) -> Result<(), filesystem::Error> { - todo!("filesystem change_file_permissions_at is not implemented") + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, + path: String, + mode: sync_filesystem::Modes, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::change_file_permissions_at( + self, + fd, + path_flags.into(), + path, + mode.into(), + ) + .await + })?) } - async fn change_directory_permissions_at( + fn change_directory_permissions_at( &mut self, - _fd: filesystem::Descriptor, - _path_flags: filesystem::PathFlags, - _path: String, - _mode: filesystem::Modes, - ) -> Result<(), filesystem::Error> { - todo!("filesystem change_directory_permissions_at is not implemented") + fd: sync_filesystem::Descriptor, + path_flags: sync_filesystem::PathFlags, + path: String, + mode: sync_filesystem::Modes, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::change_directory_permissions_at( + self, + fd, + path_flags.into(), + path, + mode.into(), + ) + .await + })?) } - async fn lock_shared(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!("filesystem lock_shared is not implemented") + fn lock_shared( + &mut self, + fd: sync_filesystem::Descriptor, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::lock_shared(self, fd).await + })?) } - async fn lock_exclusive( + fn lock_exclusive( &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { - todo!("filesystem lock_exclusive is not implemented") + fd: sync_filesystem::Descriptor, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::lock_exclusive(self, fd).await + })?) } - async fn try_lock_shared( + fn try_lock_shared( &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { - todo!("filesystem try_lock_shared is not implemented") + fd: sync_filesystem::Descriptor, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::try_lock_shared(self, fd).await + })?) } - async fn try_lock_exclusive( + fn try_lock_exclusive( &mut self, - _fd: filesystem::Descriptor, - ) -> Result<(), filesystem::Error> { - todo!("filesystem try_lock_exclusive is not implemented") + fd: sync_filesystem::Descriptor, + ) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::try_lock_exclusive(self, fd).await + })?) } - async fn unlock(&mut self, _fd: filesystem::Descriptor) -> Result<(), filesystem::Error> { - todo!("filesystem unlock is not implemented") + fn unlock(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { + Ok(block_on(async { + async_filesystem::Host::unlock(self, fd).await + })?) } - async fn read_via_stream( + fn read_via_stream( &mut self, - fd: filesystem::Descriptor, - offset: filesystem::Filesize, - ) -> Result { - // Trap if fd lookup fails: - let f = self.table().get_file(fd)?; - - if !f.perms.contains(FilePerms::READ) { - Err(filesystem::ErrorCode::BadDescriptor)?; - } - // Duplicate the file descriptor so that we get an indepenent lifetime. - let clone = std::sync::Arc::clone(&f.file); - - // Create a stream view for it. - let reader = crate::preview2::filesystem::FileInputStream::new(clone, offset); - - // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_input_stream(Box::new(reader))?; - - Ok(index) + fd: sync_filesystem::Descriptor, + offset: sync_filesystem::Filesize, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::read_via_stream(self, fd, offset).await + })?) } - async fn write_via_stream( + fn write_via_stream( &mut self, - fd: filesystem::Descriptor, - offset: filesystem::Filesize, - ) -> Result { - // Trap if fd lookup fails: - let f = self.table().get_file(fd)?; - - if !f.perms.contains(FilePerms::WRITE) { - Err(filesystem::ErrorCode::BadDescriptor)?; - } - - // Duplicate the file descriptor so that we get an indepenent lifetime. - let clone = std::sync::Arc::clone(&f.file); - - // Create a stream view for it. - let writer = crate::preview2::filesystem::FileOutputStream::new(clone, offset); - - // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_output_stream(Box::new(writer))?; - - Ok(index) + fd: sync_filesystem::Descriptor, + offset: sync_filesystem::Filesize, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::write_via_stream(self, fd, offset).await + })?) } - async fn append_via_stream( + fn append_via_stream( &mut self, - fd: filesystem::Descriptor, - ) -> Result { - // Trap if fd lookup fails: - let f = self.table().get_file(fd)?; - - if !f.perms.contains(FilePerms::WRITE) { - Err(filesystem::ErrorCode::BadDescriptor)?; - } - // Duplicate the file descriptor so that we get an indepenent lifetime. - let clone = std::sync::Arc::clone(&f.file); - - // Create a stream view for it. - let appender = crate::preview2::filesystem::FileAppendStream::new(clone); - - // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_output_stream(Box::new(appender))?; - - Ok(index) + fd: sync_filesystem::Descriptor, + ) -> Result { + Ok(block_on(async { + async_filesystem::Host::append_via_stream(self, fd).await + })?) } - */ } impl From for sync_filesystem::ErrorCode { @@ -809,6 +490,53 @@ impl From for sync_filesystem::DescriptorType DescriptorType::CharacterDevice => Self::CharacterDevice, DescriptorType::Fifo => Self::Fifo, DescriptorType::Socket => Self::Socket, + DescriptorType::SymbolicLink => Self::SymbolicLink, + DescriptorType::Unknown => Self::Unknown, } } } + +impl From for sync_filesystem::DirectoryEntry { + fn from(other: async_filesystem::DirectoryEntry) -> Self { + todo!() + } +} + +impl From for sync_filesystem::DescriptorStat { + fn from(other: async_filesystem::DescriptorStat) -> Self { + todo!() + } +} + +impl From for async_filesystem::PathFlags { + fn from(other: sync_filesystem::PathFlags) -> Self { + todo!() + } +} + +impl From for async_filesystem::NewTimestamp { + fn from(other: sync_filesystem::NewTimestamp) -> Self { + todo!() + } +} + +impl From for async_filesystem::OpenFlags { + fn from(other: sync_filesystem::OpenFlags) -> Self { + todo!() + } +} +impl From for async_filesystem::DescriptorFlags { + fn from(other: sync_filesystem::DescriptorFlags) -> Self { + todo!() + } +} +impl From for async_filesystem::Modes { + fn from(other: sync_filesystem::Modes) -> Self { + todo!() + } +} +impl From for async_filesystem::AccessType { + fn from(other: sync_filesystem::AccessType) -> Self { + todo!() + } +} From d02c6c2adc1badf3046f9b3ca020afd69d351308 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 15:37:46 -0700 Subject: [PATCH 092/118] finish type conversion boilerplate --- .../src/preview2/preview2/filesystem/sync.rs | 91 +++++++++++++++++-- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/filesystem/sync.rs b/crates/wasi/src/preview2/preview2/filesystem/sync.rs index bf44e57ff5f7..7e0909ac7e5e 100644 --- a/crates/wasi/src/preview2/preview2/filesystem/sync.rs +++ b/crates/wasi/src/preview2/preview2/filesystem/sync.rs @@ -467,14 +467,17 @@ impl From for sync_filesystem::DescriptorFlag if other.contains(async_filesystem::DescriptorFlags::WRITE) { out |= Self::WRITE; } + if other.contains(async_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } if other.contains(async_filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { out |= Self::DATA_INTEGRITY_SYNC; } if other.contains(async_filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { out |= Self::REQUESTED_WRITE_SYNC; } - if other.contains(async_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { - out |= Self::FILE_INTEGRITY_SYNC; + if other.contains(async_filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; } out } @@ -498,45 +501,113 @@ impl From for sync_filesystem::DescriptorType impl From for sync_filesystem::DirectoryEntry { fn from(other: async_filesystem::DirectoryEntry) -> Self { - todo!() + Self { + inode: other.inode, + type_: other.type_.into(), + name: other.name, + } } } impl From for sync_filesystem::DescriptorStat { fn from(other: async_filesystem::DescriptorStat) -> Self { - todo!() + Self { + device: other.device, + inode: other.inode, + type_: other.type_.into(), + link_count: other.link_count, + size: other.size, + data_access_timestamp: other.data_access_timestamp, + data_modification_timestamp: other.data_modification_timestamp, + status_change_timestamp: other.status_change_timestamp, + } } } impl From for async_filesystem::PathFlags { fn from(other: sync_filesystem::PathFlags) -> Self { - todo!() + let mut out = Self::empty(); + if other.contains(sync_filesystem::PathFlags::SYMLINK_FOLLOW) { + out |= Self::SYMLINK_FOLLOW; + } + out } } impl From for async_filesystem::NewTimestamp { fn from(other: sync_filesystem::NewTimestamp) -> Self { - todo!() + use sync_filesystem::NewTimestamp; + match other { + NewTimestamp::NoChange => Self::NoChange, + NewTimestamp::Now => Self::Now, + NewTimestamp::Timestamp(datetime) => Self::Timestamp(datetime), + } } } impl From for async_filesystem::OpenFlags { fn from(other: sync_filesystem::OpenFlags) -> Self { - todo!() + let mut out = Self::empty(); + if other.contains(sync_filesystem::OpenFlags::CREATE) { + out |= Self::CREATE; + } + if other.contains(sync_filesystem::OpenFlags::DIRECTORY) { + out |= Self::DIRECTORY; + } + if other.contains(sync_filesystem::OpenFlags::EXCLUSIVE) { + out |= Self::EXCLUSIVE; + } + if other.contains(sync_filesystem::OpenFlags::TRUNCATE) { + out |= Self::TRUNCATE; + } + out } } impl From for async_filesystem::DescriptorFlags { fn from(other: sync_filesystem::DescriptorFlags) -> Self { - todo!() + let mut out = Self::empty(); + if other.contains(sync_filesystem::DescriptorFlags::READ) { + out |= Self::READ; + } + if other.contains(sync_filesystem::DescriptorFlags::WRITE) { + out |= Self::WRITE; + } + if other.contains(sync_filesystem::DescriptorFlags::FILE_INTEGRITY_SYNC) { + out |= Self::FILE_INTEGRITY_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::DATA_INTEGRITY_SYNC) { + out |= Self::DATA_INTEGRITY_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::REQUESTED_WRITE_SYNC) { + out |= Self::REQUESTED_WRITE_SYNC; + } + if other.contains(sync_filesystem::DescriptorFlags::MUTATE_DIRECTORY) { + out |= Self::MUTATE_DIRECTORY; + } + out } } impl From for async_filesystem::Modes { fn from(other: sync_filesystem::Modes) -> Self { - todo!() + let mut out = Self::empty(); + if other.contains(sync_filesystem::Modes::READABLE) { + out |= Self::READABLE; + } + if other.contains(sync_filesystem::Modes::WRITABLE) { + out |= Self::WRITABLE; + } + if other.contains(sync_filesystem::Modes::EXECUTABLE) { + out |= Self::EXECUTABLE; + } + out } } impl From for async_filesystem::AccessType { fn from(other: sync_filesystem::AccessType) -> Self { - todo!() + use sync_filesystem::AccessType; + match other { + AccessType::Access(modes) => Self::Access(modes.into()), + AccessType::Exists => Self::Exists, + } } } From 95df882c5835cf4779c7ff47a41343876e05948d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 15:42:05 -0700 Subject: [PATCH 093/118] Revert "half-baked idea that the output-stream interface will need a flush mechanism" This reverts commit 3eb762e3330a7228318bfe01296483b52d0fdc16. --- crates/wasi/wit/deps/io/streams.wit | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/wasi/wit/deps/io/streams.wit b/crates/wasi/wit/deps/io/streams.wit index 46cee73dc5f6..0943759ab32e 100644 --- a/crates/wasi/wit/deps/io/streams.wit +++ b/crates/wasi/wit/deps/io/streams.wit @@ -163,25 +163,6 @@ interface streams { buf: list ) -> result, stream-error> - /* FIXME: i think we need some nonsense like this, because a nonblocking - * write is necessarily buffering the caller's input somewhere, and - * the caller may need to flush (block until buffer has been emptied) any - * buffers we create on its behalf. - * however this is a whole can of worms. - write-with-flush: func( - this: output-stream, - /// Data to write - buf: list - ) -> result - - // returns Some once ready: - flush-check: func(this: flush-resource) -> option, stream-error>> - // get a pollable to check readiness: - subscribe-to-flush: func(this: flush-resource) -> pollable - // drop the resource: - flush-delete: func(this: flush-resource) - */ - /// Write bytes to a stream, with blocking. /// /// This is similar to `write`, except that it blocks until at least one From 1e595311dbef1e401542ef745ada69833fafdce4 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 15:43:37 -0700 Subject: [PATCH 094/118] cargo fmt --- crates/wasi/src/preview2/preview1/mod.rs | 12 +++++++----- crates/wasi/src/preview2/stream.rs | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index 498fb01161f7..142c29b8a778 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1722,11 +1722,13 @@ impl< let dirfd = self.get_dir_fd(dirfd)?; let src_path = read_string(src_path)?; let dest_path = read_string(dest_path)?; - self.symlink_at(dirfd, src_path, dest_path).await.map_err(|e| { - e.try_into() - .context("failed to call `symlink-at`") - .unwrap_or_else(types::Error::trap) - }) + self.symlink_at(dirfd, src_path, dest_path) + .await + .map_err(|e| { + e.try_into() + .context("failed to call `symlink-at`") + .unwrap_or_else(types::Error::trap) + }) } #[instrument(skip(self))] diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index d4b7ea494580..828e236236d8 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -178,5 +178,4 @@ mod test { let ix = table.push_output_stream(Box::new(dummy)).unwrap(); let _ = table.get_output_stream_mut(ix).unwrap(); } - } From 5abe3f0410ae7bf547177750b4f9a937ceb1e914 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 15:57:23 -0700 Subject: [PATCH 095/118] test type fixes --- crates/test-programs/tests/command.rs | 8 ++++++-- crates/test-programs/tests/reactor.rs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index 5d00df42e185..21f020e317e0 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -173,7 +173,9 @@ async fn time() -> Result<()> { async fn stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() - .set_stdin(MemoryInputPipe::new("So rested he by the Tumtum tree")) + .set_stdin(MemoryInputPipe::new( + "So rested he by the Tumtum tree".into(), + )) .build(&mut table)?; let (mut store, command) = @@ -189,7 +191,9 @@ async fn stdin() -> Result<()> { async fn poll_stdin() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() - .set_stdin(MemoryInputPipe::new("So rested he by the Tumtum tree")) + .set_stdin(MemoryInputPipe::new( + "So rested he by the Tumtum tree".into(), + )) .build(&mut table)?; let (mut store, command) = diff --git a/crates/test-programs/tests/reactor.rs b/crates/test-programs/tests/reactor.rs index ad5ab5213a6a..87e1a6925e0d 100644 --- a/crates/test-programs/tests/reactor.rs +++ b/crates/test-programs/tests/reactor.rs @@ -116,7 +116,7 @@ async fn reactor_tests() -> Result<()> { let r = reactor.call_write_strings_to(&mut store, table_ix).await?; assert_eq!(r, Ok(())); - assert_eq!(writepipe.contents(), b"hellogussie"); + assert_eq!(writepipe.contents().as_ref(), b"hellogussie"); // Show that the `with` invocation in the macro means we get to re-use the // type definitions from inside the `host` crate for these structures: From 84742b09c67e2839b573a3e2982811cf397de614 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 16:13:53 -0700 Subject: [PATCH 096/118] renames and comments --- crates/wasi/src/preview2/filesystem.rs | 10 ++- .../wasi/src/preview2/preview2/filesystem.rs | 68 +++++++++++-------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 8aa3f63bffb9..6412de5b3291 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -13,6 +13,8 @@ bitflags::bitflags! { } pub(crate) struct File { + /// Wrapped in an Arc because the same underlying file is used for + /// implementing the stream types. Also needed for [`block`]. pub file: Arc, pub perms: FilePerms, } @@ -25,7 +27,9 @@ impl File { } } - pub(crate) async fn block(&self, body: F) -> R + /// Spawn a task on tokio's blocking thread for performing blocking + /// syscalls on the underlying [`cap_std::fs::File`]. + pub(crate) async fn spawn_blocking(&self, body: F) -> R where F: FnOnce(&cap_std::fs::File) -> R + Send + 'static, R: Send + 'static, @@ -97,7 +101,9 @@ impl Dir { } } - pub(crate) async fn block(&self, body: F) -> R + /// Spawn a task on tokio's blocking thread for performing blocking + /// syscalls on the underlying [`cap_std::fs::Dir`]. + pub(crate) async fn spawn_blocking(&self, body: F) -> R where F: FnOnce(&cap_std::fs::Dir) -> R + Send + 'static, R: Send + 'static, diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index 72b3e6ea13e0..d646c3a13aca 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -45,7 +45,8 @@ impl filesystem::Host for T { }; let f = self.table().get_file(fd)?; - f.block(move |f| f.advise(offset, len, advice)).await?; + f.spawn_blocking(move |f| f.advise(offset, len, advice)) + .await?; Ok(()) } @@ -53,7 +54,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match f.block(|f| f.sync_data()).await { + match f.spawn_blocking(|f| f.sync_data()).await { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -69,7 +70,7 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_data()?)) + d.spawn_blocking(|d| Ok(d.open(std::path::Component::CurDir)?.sync_data()?)) .await } else { Err(ErrorCode::BadDescriptor.into()) @@ -100,7 +101,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - let flags = f.block(|f| f.get_fd_flags()).await?; + let flags = f.spawn_blocking(|f| f.get_fd_flags()).await?; let mut flags = get_from_fdflags(flags); if f.perms.contains(FilePerms::READ) { flags |= DescriptorFlags::READ; @@ -111,7 +112,7 @@ impl filesystem::Host for T { Ok(flags) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - let flags = d.block(|d| d.get_fd_flags()).await?; + let flags = d.spawn_blocking(|d| d.get_fd_flags()).await?; let mut flags = get_from_fdflags(flags); if d.perms.contains(DirPerms::READ) { flags |= DescriptorFlags::READ; @@ -133,7 +134,7 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; - let meta = f.block(|f| f.metadata()).await?; + let meta = f.spawn_blocking(|f| f.metadata()).await?; Ok(descriptortype_from(meta.file_type())) } else if table.is_dir(fd) { Ok(filesystem::DescriptorType::Directory) @@ -151,7 +152,7 @@ impl filesystem::Host for T { if !f.perms.contains(FilePerms::WRITE) { Err(ErrorCode::NotPermitted)?; } - f.block(move |f| f.set_len(size)).await?; + f.spawn_blocking(move |f| f.set_len(size)).await?; Ok(()) } @@ -171,7 +172,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - f.block(|f| f.set_times(atim, mtim)).await?; + f.spawn_blocking(|f| f.set_times(atim, mtim)).await?; Ok(()) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; @@ -180,7 +181,7 @@ impl filesystem::Host for T { } let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; - d.block(|d| d.set_times(atim, mtim)).await?; + d.spawn_blocking(|d| d.set_times(atim, mtim)).await?; Ok(()) } else { Err(ErrorCode::BadDescriptor.into()) @@ -204,7 +205,7 @@ impl filesystem::Host for T { } let (mut buffer, r) = f - .block(move |f| { + .spawn_blocking(move |f| { let mut buffer = vec![0; len.try_into().unwrap_or(usize::MAX)]; let r = f.read_vectored_at(&mut [IoSliceMut::new(&mut buffer)], offset); (buffer, r) @@ -238,7 +239,7 @@ impl filesystem::Host for T { } let bytes_written = f - .block(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) + .spawn_blocking(move |f| f.write_vectored_at(&[IoSlice::new(&buf)], offset)) .await?; Ok(filesystem::Filesize::try_from(bytes_written).expect("usize fits in Filesize")) @@ -267,7 +268,7 @@ impl filesystem::Host for T { } let entries = d - .block(|d| { + .spawn_blocking(|d| { // Both `entries` and `full_metadata` perform syscalls, which is why they are done // within this `block` call, rather than delay calculating the full metadata // for entries when they're demanded later in the iterator chain. @@ -333,7 +334,7 @@ impl filesystem::Host for T { let table = self.table(); if table.is_file(fd) { let f = table.get_file(fd)?; - match f.block(|f| f.sync_all()).await { + match f.spawn_blocking(|f| f.sync_all()).await { Ok(()) => Ok(()), // On windows, `sync_data` uses `FileFlushBuffers` which fails with // `ERROR_ACCESS_DENIED` if the file is not upen for writing. Ignore @@ -349,7 +350,7 @@ impl filesystem::Host for T { } } else if table.is_dir(fd) { let d = table.get_dir(fd)?; - d.block(|d| Ok(d.open(std::path::Component::CurDir)?.sync_all()?)) + d.spawn_blocking(|d| Ok(d.open(std::path::Component::CurDir)?.sync_all()?)) .await } else { Err(ErrorCode::BadDescriptor.into()) @@ -366,7 +367,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - d.block(move |d| d.create_dir(&path)).await?; + d.spawn_blocking(move |d| d.create_dir(&path)).await?; Ok(()) } @@ -378,12 +379,12 @@ impl filesystem::Host for T { if table.is_file(fd) { let f = table.get_file(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = f.block(|f| f.metadata()).await?; + let meta = f.spawn_blocking(|f| f.metadata()).await?; Ok(descriptorstat_from(meta)) } else if table.is_dir(fd) { let d = table.get_dir(fd)?; // No permissions check on stat: if opened, allowed to stat it - let meta = d.block(|d| d.dir_metadata()).await?; + let meta = d.spawn_blocking(|d| d.dir_metadata()).await?; Ok(descriptorstat_from(meta)) } else { Err(ErrorCode::BadDescriptor.into()) @@ -403,9 +404,9 @@ impl filesystem::Host for T { } let meta = if symlink_follow(path_flags) { - d.block(move |d| d.metadata(&path)).await? + d.spawn_blocking(move |d| d.metadata(&path)).await? } else { - d.block(move |d| d.symlink_metadata(&path)).await? + d.spawn_blocking(move |d| d.symlink_metadata(&path)).await? }; Ok(descriptorstat_from(meta)) } @@ -428,7 +429,7 @@ impl filesystem::Host for T { let atim = systemtimespec_from(atim)?; let mtim = systemtimespec_from(mtim)?; if symlink_follow(path_flags) { - d.block(move |d| { + d.spawn_blocking(move |d| { d.set_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), @@ -437,7 +438,7 @@ impl filesystem::Host for T { }) .await?; } else { - d.block(move |d| { + d.spawn_blocking(move |d| { d.set_symlink_times( &path, atim.map(cap_fs_ext::SystemTimeSpec::from_std), @@ -472,7 +473,7 @@ impl filesystem::Host for T { } let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); old_dir - .block(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) + .spawn_blocking(move |d| d.hard_link(&old_path, &new_dir_handle, &new_path)) .await?; Ok(()) } @@ -556,6 +557,9 @@ impl filesystem::Host for T { } } + // Represents each possible outcome from the spawn_blocking operation. + // This makes sure we don't have to give spawn_blocking any way to + // manipulate the table. enum OpenResult { Dir(cap_std::fs::Dir), File(cap_std::fs::File), @@ -563,7 +567,7 @@ impl filesystem::Host for T { } let opened = d - .block::<_, std::io::Result>(move |d| { + .spawn_blocking::<_, std::io::Result>(move |d| { let mut opened = d.open_with(&path, &opts)?; if opened.metadata()?.is_dir() { Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file( @@ -595,7 +599,11 @@ impl filesystem::Host for T { async fn drop_descriptor(&mut self, fd: filesystem::Descriptor) -> anyhow::Result<()> { let table = self.table_mut(); - // Table operations don't need to go in the background thread. + // The Drop will close the file/dir, but if the close syscall + // blocks the thread, I will face god and walk backwards into hell. + // tokio::fs::File just uses std::fs::File's Drop impl to close, so + // it doesn't appear anyone else has found this to be a problem. + // (Not that they could solve it without async drop...) if table.delete_file(fd).is_err() { table.delete_dir(fd)?; } @@ -613,7 +621,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::READ) { return Err(ErrorCode::NotPermitted.into()); } - let link = d.block(move |d| d.read_link(&path)).await?; + let link = d.spawn_blocking(move |d| d.read_link(&path)).await?; Ok(link .into_os_string() .into_string() @@ -630,7 +638,7 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(move |d| d.remove_dir(&path)).await?) + Ok(d.spawn_blocking(move |d| d.remove_dir(&path)).await?) } async fn rename_at( @@ -651,7 +659,7 @@ impl filesystem::Host for T { } let new_dir_handle = std::sync::Arc::clone(&new_dir.dir); Ok(old_dir - .block(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) + .spawn_blocking(move |d| d.rename(&old_path, &new_dir_handle, &new_path)) .await?) } @@ -670,7 +678,8 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(move |d| d.symlink(&src_path, &dest_path)).await?) + Ok(d.spawn_blocking(move |d| d.symlink(&src_path, &dest_path)) + .await?) } async fn unlink_file_at( @@ -685,7 +694,8 @@ impl filesystem::Host for T { if !d.perms.contains(DirPerms::MUTATE) { return Err(ErrorCode::NotPermitted.into()); } - Ok(d.block(move |d| d.remove_file_or_symlink(&path)).await?) + Ok(d.spawn_blocking(move |d| d.remove_file_or_symlink(&path)) + .await?) } async fn access_at( From b577f74491d550b0f0ce5120b0da5f4bd88d2c29 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 19 Jul 2023 17:02:39 -0700 Subject: [PATCH 097/118] refactor stream table internals so we can have a blocking variant... --- crates/wasi/src/preview2/preview2/io.rs | 16 ++-- crates/wasi/src/preview2/stream.rs | 117 +++++++++++++++++++----- crates/wasi/src/preview2/table.rs | 16 ++++ 3 files changed, 116 insertions(+), 33 deletions(-) diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index bf75f2b406c0..5da60e0af158 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -43,14 +43,12 @@ const ZEROS: &[u8] = &[0; 4 * 1024 * 1024]; #[async_trait::async_trait] impl streams::Host for T { async fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { - self.table_mut() - .delete::>(stream)?; + self.table_mut().delete_input_stream(stream)?; Ok(()) } async fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { - self.table_mut() - .delete::>(stream)?; + self.table_mut().delete_output_stream(stream)?; Ok(()) } @@ -61,7 +59,7 @@ impl streams::Host for T { ) -> Result<(Vec, streams::StreamStatus), streams::Error> { let s = self.table_mut().get_input_stream_mut(stream)?; - let (bytes, state) = HostInputStream::read(s.as_mut(), len as usize)?; + let (bytes, state) = HostInputStream::read(s, len as usize)?; debug_assert!(bytes.len() <= len as usize); Ok((bytes.into(), state.into())) @@ -86,7 +84,7 @@ impl streams::Host for T { ) -> Result<(u64, streams::StreamStatus), streams::Error> { let s = self.table_mut().get_output_stream_mut(stream)?; - let (bytes_written, status) = HostOutputStream::write(s.as_mut(), bytes.into())?; + let (bytes_written, status) = HostOutputStream::write(s, bytes.into())?; Ok((u64::try_from(bytes_written).unwrap(), status.into())) } @@ -102,7 +100,7 @@ impl streams::Host for T { let mut nwritten: usize = 0; loop { s.ready().await?; - let (written, state) = HostOutputStream::write(s.as_mut(), bytes.clone())?; + let (written, state) = HostOutputStream::write(s, bytes.clone())?; let _ = bytes.split_to(written); nwritten += written; if bytes.is_empty() || state == StreamState::Closed { @@ -144,7 +142,7 @@ impl streams::Host for T { let s = self.table_mut().get_output_stream_mut(stream)?; let mut bytes = bytes::Bytes::from_static(ZEROS); bytes.truncate((len as usize).min(bytes.len())); - let (written, state) = HostOutputStream::write(s.as_mut(), bytes)?; + let (written, state) = HostOutputStream::write(s, bytes)?; Ok((written as u64, state.into())) } @@ -159,7 +157,7 @@ impl streams::Host for T { s.ready().await?; let mut bytes = bytes::Bytes::from_static(ZEROS); bytes.truncate(remaining.min(bytes.len())); - let (written, state) = HostOutputStream::write(s.as_mut(), bytes)?; + let (written, state) = HostOutputStream::write(s, bytes)?; remaining -= written; if remaining == 0 || state == StreamState::Closed { return Ok((len - remaining as u64, state.into())); diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 828e236236d8..54c4dda33bdc 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -89,47 +89,92 @@ pub trait HostOutputStream: Send + Sync { async fn ready(&mut self) -> Result<(), Error>; } +#[async_trait::async_trait] +pub(crate) trait BlockingInputStream: Send + Sync { + async fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error>; +} + +pub(crate) enum InternalInputStream { + Host(Box), + Blocking(Box), +} + +#[async_trait::async_trait] +pub(crate) trait BlockingOutputStream: Send + Sync { + async fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error>; +} + +pub(crate) enum InternalOutputStream { + Host(Box), + Blocking(Box), +} + /// Extension trait for managing [`HostInputStream`]s and [`HostOutputStream`]s in the [`Table`]. pub trait TableStreamExt { /// Push a [`HostInputStream`] into a [`Table`], returning the table index. fn push_input_stream(&mut self, istream: Box) -> Result; /// Get a mutable reference to a [`HostInputStream`] in a [`Table`]. - fn get_input_stream_mut( - &mut self, - fd: u32, - ) -> Result<&mut Box, TableError>; + fn get_input_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostInputStream, TableError>; + /// Remove [`HostInputStream`] from table: + fn delete_input_stream(&mut self, fd: u32) -> Result, TableError>; /// Push a [`HostOutputStream`] into a [`Table`], returning the table index. fn push_output_stream(&mut self, ostream: Box) -> Result; /// Get a mutable reference to a [`HostOutputStream`] in a [`Table`]. - fn get_output_stream_mut( - &mut self, - fd: u32, - ) -> Result<&mut Box, TableError>; + fn get_output_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostOutputStream, TableError>; + + /// Remove [`HostOutputStream`] from table: + fn delete_output_stream(&mut self, fd: u32) -> Result, TableError>; } impl TableStreamExt for Table { fn push_input_stream(&mut self, istream: Box) -> Result { - self.push(Box::new(istream)) + self.push(Box::new(InternalInputStream::Host(istream))) } - fn get_input_stream_mut( - &mut self, - fd: u32, - ) -> Result<&mut Box, TableError> { - self.get_mut::>(fd) + fn get_input_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostInputStream, TableError> { + match self.get_mut::(fd)? { + InternalInputStream::Host(ref mut h) => Ok(h.as_mut()), + _ => Err(TableError::WrongType), + } + } + fn delete_input_stream(&mut self, fd: u32) -> Result, TableError> { + let occ = self.entry(fd)?; + match occ.get().downcast_ref::() { + Some(InternalInputStream::Host(_)) => { + let (_, any) = occ.remove_entry(); + match *any.downcast().expect("downcast checked above") { + InternalInputStream::Host(h) => Ok(h), + _ => unreachable!("variant checked above"), + } + } + _ => Err(TableError::WrongType), + } } fn push_output_stream( &mut self, ostream: Box, ) -> Result { - self.push(Box::new(ostream)) + self.push(Box::new(InternalOutputStream::Host(ostream))) } - fn get_output_stream_mut( - &mut self, - fd: u32, - ) -> Result<&mut Box, TableError> { - self.get_mut::>(fd) + fn get_output_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostOutputStream, TableError> { + match self.get_mut::(fd)? { + InternalOutputStream::Host(ref mut h) => Ok(h.as_mut()), + _ => Err(TableError::WrongType), + } + } + fn delete_output_stream(&mut self, fd: u32) -> Result, TableError> { + let occ = self.entry(fd)?; + match occ.get().downcast_ref::() { + Some(InternalOutputStream::Host(_)) => { + let (_, any) = occ.remove_entry(); + match *any.downcast().expect("downcast checked above") { + InternalOutputStream::Host(h) => Ok(h), + _ => unreachable!("variant checked above"), + } + } + _ => Err(TableError::WrongType), + } } } @@ -152,10 +197,22 @@ mod test { let dummy = DummyInputStream; let mut table = Table::new(); - // Show that we can put an input stream in the table, and get a mut - // ref back out: + // Put it into the table: let ix = table.push_input_stream(Box::new(dummy)).unwrap(); + // Get a mut ref to it: let _ = table.get_input_stream_mut(ix).unwrap(); + // Fails at wrong type: + assert!(matches!( + table.get_output_stream_mut(ix), + Err(TableError::WrongType) + )); + // Delete it: + let _ = table.delete_input_stream(ix).unwrap(); + // Now absent from table: + assert!(matches!( + table.get_input_stream_mut(ix), + Err(TableError::NotPresent) + )); } #[test] @@ -173,9 +230,21 @@ mod test { let dummy = DummyOutputStream; let mut table = Table::new(); - // Show that we can put an output stream in the table, and get a mut - // ref back out: + // Put it in the table: let ix = table.push_output_stream(Box::new(dummy)).unwrap(); + // Get a mut ref to it: let _ = table.get_output_stream_mut(ix).unwrap(); + // Fails at wrong type: + assert!(matches!( + table.get_input_stream_mut(ix), + Err(TableError::WrongType) + )); + // Delete it: + let _ = table.delete_output_stream(ix).unwrap(); + // Now absent: + assert!(matches!( + table.get_output_stream_mut(ix), + Err(TableError::NotPresent) + )); } } diff --git a/crates/wasi/src/preview2/table.rs b/crates/wasi/src/preview2/table.rs index 79dbc6746bcd..675850d7df4b 100644 --- a/crates/wasi/src/preview2/table.rs +++ b/crates/wasi/src/preview2/table.rs @@ -87,6 +87,22 @@ impl Table { } } + /// Get an [`OccupiedEntry`] corresponding to a table entry, if it exists. This allows you to + /// remove or replace the entry based on its contents. + pub fn entry( + &mut self, + key: u32, + ) -> Result< + std::collections::hash_map::OccupiedEntry>, + TableError, + > { + use std::collections::hash_map::Entry; + match self.map.entry(key) { + Entry::Occupied(occ) => Ok(occ), + Entry::Vacant(_) => Err(TableError::NotPresent), + } + } + /// Remove a resource at a given index from the table. pub fn delete(&mut self, key: u32) -> Result { // Optimistically attempt to remove the value stored under key From 4bbf66919d680c48321d7ae9747b4ca9a8adb799 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 10:44:55 -0700 Subject: [PATCH 098/118] preview1 host adapter: stdout/stderr use blocking_write here too --- crates/wasi/src/preview2/preview1/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/preview1/mod.rs b/crates/wasi/src/preview2/preview1/mod.rs index 142c29b8a778..0bbbe1c9b67d 100644 --- a/crates/wasi/src/preview2/preview1/mod.rs +++ b/crates/wasi/src/preview2/preview1/mod.rs @@ -1214,7 +1214,7 @@ impl< let Some(buf) = first_non_empty_ciovec(ciovs)? else { return Ok(0) }; - let (n, _stat) = streams::Host::write(self, stream, buf) + let (n, _stat) = streams::Host::blocking_write(self, stream, buf) .await .map_err(|_| types::Errno::Io)?; n From b5408147dc2e0de46fd5c30ff109f3694be59438 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 12:47:25 -0700 Subject: [PATCH 099/118] filesystem streams are blocking now --- crates/wasi/src/preview2/filesystem.rs | 93 ++++---- .../wasi/src/preview2/preview2/filesystem.rs | 35 ++- crates/wasi/src/preview2/preview2/io.rs | 200 +++++++++++------- crates/wasi/src/preview2/stream.rs | 84 ++++++-- 4 files changed, 267 insertions(+), 145 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 6412de5b3291..f519d7011ae5 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,6 +1,4 @@ -use crate::preview2::{ - block_in_place, HostInputStream, HostOutputStream, StreamState, Table, TableError, -}; +use crate::preview2::{StreamState, Table, TableError}; use bytes::{Bytes, BytesMut}; use std::sync::Arc; @@ -121,23 +119,23 @@ impl FileInputStream { pub fn new(file: Arc, position: u64) -> Self { Self { file, position } } -} -#[async_trait::async_trait] -impl HostInputStream for FileInputStream { - fn read(&mut self, size: usize) -> anyhow::Result<(Bytes, StreamState)> { + pub async fn read(&mut self, size: usize) -> anyhow::Result<(Bytes, StreamState)> { use system_interface::fs::FileIoExt; - let mut buf = BytesMut::zeroed(size); - let (n, state) = read_result(block_in_place(|| { - self.file.read_at(&mut buf, self.position) - }))?; + let f = Arc::clone(&self.file); + let p = self.position; + let (r, mut buf) = tokio::task::spawn_blocking(move || { + let mut buf = BytesMut::zeroed(size); + let r = f.read_at(&mut buf, p); + (r, buf) + }) + .await + .unwrap(); + let (n, state) = read_result(r)?; buf.truncate(n); self.position += n as u64; Ok((buf.freeze(), state)) } - async fn ready(&mut self) -> anyhow::Result<()> { - Ok(()) // Always immediately ready - file reads cannot block - } } pub(crate) fn read_result( @@ -161,49 +159,44 @@ pub(crate) fn write_result( } } +#[derive(Clone, Copy)] +pub(crate) enum FileOutputMode { + Position(u64), + Append, +} + pub(crate) struct FileOutputStream { file: Arc, - position: u64, + mode: FileOutputMode, } impl FileOutputStream { - pub fn new(file: Arc, position: u64) -> Self { - Self { file, position } - } -} - -#[async_trait::async_trait] -impl HostOutputStream for FileOutputStream { - /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { - use system_interface::fs::FileIoExt; - let (n, state) = write_result(block_in_place(|| self.file.write_at(&buf, self.position)))?; - self.position += n as u64; - Ok((n, state)) - } - - async fn ready(&mut self) -> anyhow::Result<()> { - Ok(()) // Always immediately ready - file writes cannot block + pub fn write_at(file: Arc, position: u64) -> Self { + Self { + file, + mode: FileOutputMode::Position(position), + } } -} - -pub(crate) struct FileAppendStream { - file: Arc, -} -impl FileAppendStream { - pub fn new(file: Arc) -> Self { - Self { file } + pub fn append(file: Arc) -> Self { + Self { + file, + mode: FileOutputMode::Append, + } } -} - -#[async_trait::async_trait] -impl HostOutputStream for FileAppendStream { /// Write bytes. On success, returns the number of bytes written. - fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { + pub async fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { use system_interface::fs::FileIoExt; - Ok(write_result(block_in_place(|| self.file.append(&buf)))?) - } - - async fn ready(&mut self) -> anyhow::Result<()> { - Ok(()) // Always immediately ready - file appends cannot block + let f = Arc::clone(&self.file); + let m = self.mode; + let r = tokio::task::spawn_blocking(move || match m { + FileOutputMode::Position(p) => f.write_at(buf.as_ref(), p), + FileOutputMode::Append => f.append(buf.as_ref()), + }) + .await + .unwrap(); + let (n, state) = write_result(r)?; + if let FileOutputMode::Position(ref mut position) = self.mode { + *position += n as u64; + } + Ok((n, state)) } } diff --git a/crates/wasi/src/preview2/preview2/filesystem.rs b/crates/wasi/src/preview2/preview2/filesystem.rs index d646c3a13aca..6d582e1e62f5 100644 --- a/crates/wasi/src/preview2/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/preview2/filesystem.rs @@ -2,7 +2,7 @@ use crate::preview2::bindings::clocks::wall_clock; use crate::preview2::bindings::filesystem::filesystem; use crate::preview2::bindings::io::streams; use crate::preview2::filesystem::{Dir, File, TableFsExt}; -use crate::preview2::{DirPerms, FilePerms, Table, TableError, TableStreamExt, WasiView}; +use crate::preview2::{DirPerms, FilePerms, Table, TableError, WasiView}; use filesystem::ErrorCode; @@ -762,6 +762,11 @@ impl filesystem::Host for T { fd: filesystem::Descriptor, offset: filesystem::Filesize, ) -> Result { + use crate::preview2::{ + filesystem::FileInputStream, + stream::{InternalInputStream, InternalTableStreamExt}, + }; + // Trap if fd lookup fails: let f = self.table().get_file(fd)?; @@ -772,10 +777,12 @@ impl filesystem::Host for T { let clone = std::sync::Arc::clone(&f.file); // Create a stream view for it. - let reader = crate::preview2::filesystem::FileInputStream::new(clone, offset); + let reader = FileInputStream::new(clone, offset); // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_input_stream(Box::new(reader))?; + let index = self + .table_mut() + .push_internal_input_stream(InternalInputStream::File(reader))?; Ok(index) } @@ -785,6 +792,11 @@ impl filesystem::Host for T { fd: filesystem::Descriptor, offset: filesystem::Filesize, ) -> Result { + use crate::preview2::{ + filesystem::FileOutputStream, + stream::{InternalOutputStream, InternalTableStreamExt}, + }; + // Trap if fd lookup fails: let f = self.table().get_file(fd)?; @@ -796,10 +808,12 @@ impl filesystem::Host for T { let clone = std::sync::Arc::clone(&f.file); // Create a stream view for it. - let writer = crate::preview2::filesystem::FileOutputStream::new(clone, offset); + let writer = FileOutputStream::write_at(clone, offset); // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_output_stream(Box::new(writer))?; + let index = self + .table_mut() + .push_internal_output_stream(InternalOutputStream::File(writer))?; Ok(index) } @@ -808,6 +822,11 @@ impl filesystem::Host for T { &mut self, fd: filesystem::Descriptor, ) -> Result { + use crate::preview2::{ + filesystem::FileOutputStream, + stream::{InternalOutputStream, InternalTableStreamExt}, + }; + // Trap if fd lookup fails: let f = self.table().get_file(fd)?; @@ -818,10 +837,12 @@ impl filesystem::Host for T { let clone = std::sync::Arc::clone(&f.file); // Create a stream view for it. - let appender = crate::preview2::filesystem::FileAppendStream::new(clone); + let appender = FileOutputStream::append(clone); // Insert the stream view into the table. Trap if the table is full. - let index = self.table_mut().push_output_stream(Box::new(appender))?; + let index = self + .table_mut() + .push_internal_output_stream(InternalOutputStream::File(appender))?; Ok(index) } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 5da60e0af158..411779455f5b 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -1,8 +1,12 @@ use crate::preview2::{ bindings::io::streams::{self, InputStream, OutputStream, StreamError}, bindings::poll::poll::Pollable, + filesystem::{FileInputStream, FileOutputStream}, poll::PollableFuture, - stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}, + stream::{ + HostInputStream, HostOutputStream, InternalInputStream, InternalOutputStream, + InternalTableStreamExt, StreamState, + }, HostPollable, TableError, TablePollableExt, WasiView, }; use anyhow::anyhow; @@ -43,12 +47,12 @@ const ZEROS: &[u8] = &[0; 4 * 1024 * 1024]; #[async_trait::async_trait] impl streams::Host for T { async fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { - self.table_mut().delete_input_stream(stream)?; + self.table_mut().delete_internal_input_stream(stream)?; Ok(()) } async fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { - self.table_mut().delete_output_stream(stream)?; + self.table_mut().delete_internal_output_stream(stream)?; Ok(()) } @@ -57,12 +61,18 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { - let s = self.table_mut().get_input_stream_mut(stream)?; + match self.table_mut().get_internal_input_stream_mut(stream)? { + InternalInputStream::Host(s) => { + let (bytes, state) = HostInputStream::read(s.as_mut(), len as usize)?; + debug_assert!(bytes.len() <= len as usize); - let (bytes, state) = HostInputStream::read(s, len as usize)?; - debug_assert!(bytes.len() <= len as usize); - - Ok((bytes.into(), state.into())) + Ok((bytes.into(), state.into())) + } + InternalInputStream::File(s) => { + let (bytes, state) = FileInputStream::read(s, len as usize).await?; + Ok((bytes.into(), state.into())) + } + } } async fn blocking_read( @@ -70,11 +80,18 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { - self.table_mut() - .get_input_stream_mut(stream)? - .ready() - .await?; - self.read(stream, len).await + match self.table_mut().get_internal_input_stream_mut(stream)? { + InternalInputStream::Host(s) => { + s.ready().await?; + let (bytes, state) = HostInputStream::read(s.as_mut(), len as usize)?; + debug_assert!(bytes.len() <= len as usize); + Ok((bytes.into(), state.into())) + } + InternalInputStream::File(s) => { + let (bytes, state) = FileInputStream::read(s, len as usize).await?; + Ok((bytes.into(), state.into())) + } + } } async fn write( @@ -82,11 +99,16 @@ impl streams::Host for T { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let s = self.table_mut().get_output_stream_mut(stream)?; - - let (bytes_written, status) = HostOutputStream::write(s, bytes.into())?; - - Ok((u64::try_from(bytes_written).unwrap(), status.into())) + match self.table_mut().get_internal_output_stream_mut(stream)? { + InternalOutputStream::Host(s) => { + let (bytes_written, status) = HostOutputStream::write(s.as_mut(), bytes.into())?; + Ok((u64::try_from(bytes_written).unwrap(), status.into())) + } + InternalOutputStream::File(s) => { + let (nwritten, state) = FileOutputStream::write(s, bytes.into()).await?; + Ok((nwritten as u64, state.into())) + } + } } async fn blocking_write( @@ -94,17 +116,23 @@ impl streams::Host for T { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let s = self.table_mut().get_output_stream_mut(stream)?; - - let mut bytes = bytes::Bytes::from(bytes); - let mut nwritten: usize = 0; - loop { - s.ready().await?; - let (written, state) = HostOutputStream::write(s, bytes.clone())?; - let _ = bytes.split_to(written); - nwritten += written; - if bytes.is_empty() || state == StreamState::Closed { - return Ok((nwritten as u64, state.into())); + match self.table_mut().get_internal_output_stream_mut(stream)? { + InternalOutputStream::Host(s) => { + let mut bytes = bytes::Bytes::from(bytes); + let mut nwritten: usize = 0; + loop { + s.ready().await?; + let (written, state) = HostOutputStream::write(s.as_mut(), bytes.clone())?; + let _ = bytes.split_to(written); + nwritten += written; + if bytes.is_empty() || state == StreamState::Closed { + return Ok((nwritten as u64, state.into())); + } + } + } + InternalOutputStream::File(s) => { + let (written, state) = FileOutputStream::write(s, bytes.into()).await?; + Ok((written as u64, state.into())) } } } @@ -114,12 +142,15 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let s = self.table_mut().get_input_stream_mut(stream)?; - - // TODO: the cast to usize should be fallible, use `.try_into()?` - let (bytes_skipped, state) = s.skip(len as usize)?; + match self.table_mut().get_internal_input_stream_mut(stream)? { + InternalInputStream::Host(s) => { + // TODO: the cast to usize should be fallible, use `.try_into()?` + let (bytes_skipped, state) = HostInputStream::skip(s.as_mut(), len as usize)?; - Ok((bytes_skipped as u64, state.into())) + Ok((bytes_skipped as u64, state.into())) + } + InternalInputStream::File(_) => todo!(), + } } async fn blocking_skip( @@ -127,11 +158,16 @@ impl streams::Host for T { stream: InputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - self.table_mut() - .get_input_stream_mut(stream)? - .ready() - .await?; - self.skip(stream, len).await + match self.table_mut().get_internal_input_stream_mut(stream)? { + InternalInputStream::Host(s) => { + s.ready().await?; + // TODO: the cast to usize should be fallible, use `.try_into()?` + let (bytes_skipped, state) = HostInputStream::skip(s.as_mut(), len as usize)?; + + Ok((bytes_skipped as u64, state.into())) + } + InternalInputStream::File(_) => todo!(), + } } async fn write_zeroes( @@ -139,10 +175,13 @@ impl streams::Host for T { stream: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - let s = self.table_mut().get_output_stream_mut(stream)?; + let s = self.table_mut().get_internal_output_stream_mut(stream)?; let mut bytes = bytes::Bytes::from_static(ZEROS); bytes.truncate((len as usize).min(bytes.len())); - let (written, state) = HostOutputStream::write(s, bytes)?; + let (written, state) = match s { + InternalOutputStream::Host(s) => HostOutputStream::write(s.as_mut(), bytes)?, + InternalOutputStream::File(s) => FileOutputStream::write(s, bytes).await?, + }; Ok((written as u64, state.into())) } @@ -152,12 +191,17 @@ impl streams::Host for T { len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { let mut remaining = len as usize; - let s = self.table_mut().get_output_stream_mut(stream)?; + let s = self.table_mut().get_internal_output_stream_mut(stream)?; loop { - s.ready().await?; + if let InternalOutputStream::Host(s) = s { + HostOutputStream::ready(s.as_mut()).await?; + } let mut bytes = bytes::Bytes::from_static(ZEROS); bytes.truncate(remaining.min(bytes.len())); - let (written, state) = HostOutputStream::write(s, bytes)?; + let (written, state) = match s { + InternalOutputStream::Host(s) => HostOutputStream::write(s.as_mut(), bytes)?, + InternalOutputStream::File(s) => FileOutputStream::write(s, bytes).await?, + }; remaining -= written; if remaining == 0 || state == StreamState::Closed { return Ok((len - remaining as u64, state.into())); @@ -236,20 +280,27 @@ impl streams::Host for T { async fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { // Ensure that table element is an input-stream: - let _ = self.table_mut().get_input_stream_mut(stream)?; - - fn input_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { - let stream = stream - .downcast_mut::>() - // Should be impossible because we made sure this will downcast to a - // HostImputStream with table check above. - .expect("downcast to HostInputStream failed"); - stream.ready() - } - - let pollable = HostPollable::TableEntry { - index: stream, - make_future: input_stream_ready, + let pollable = match self.table_mut().get_internal_input_stream_mut(stream)? { + InternalInputStream::Host(_) => { + fn input_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + let stream = stream + .downcast_mut::>() + // Should be impossible because we made sure this will downcast to a + // HostImputStream with table check above. + .expect("downcast to HostInputStream failed"); + stream.ready() + } + + HostPollable::TableEntry { + index: stream, + make_future: input_stream_ready, + } + } + // Files are always "ready" immediately (because we have no way to actually wait on + // readiness in epoll) + InternalInputStream::File(_) => { + HostPollable::Closure(Box::new(|| Box::pin(futures::future::ready(Ok(()))))) + } }; Ok(self.table_mut().push_host_pollable(pollable)?) } @@ -259,20 +310,25 @@ impl streams::Host for T { stream: OutputStream, ) -> anyhow::Result { // Ensure that table element is an output-stream: - let _ = self.table_mut().get_output_stream_mut(stream)?; - - fn output_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { - let stream = stream - .downcast_mut::>() - // Should be impossible because we made sure this will downcast to a - // HostOutputStream with table check above. - .expect("downcast to HostOutputStream failed"); - stream.ready() - } - - let pollable = HostPollable::TableEntry { - index: stream, - make_future: output_stream_ready, + let pollable = match self.table_mut().get_internal_output_stream_mut(stream)? { + InternalOutputStream::Host(_) => { + fn output_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + let stream = stream + .downcast_mut::>() + // Should be impossible because we made sure this will downcast to a + // HostOutputStream with table check above. + .expect("downcast to HostOutputStream failed"); + stream.ready() + } + + HostPollable::TableEntry { + index: stream, + make_future: output_stream_ready, + } + } + InternalOutputStream::File(_) => { + HostPollable::Closure(Box::new(|| Box::pin(futures::future::ready(Ok(()))))) + } }; Ok(self.table_mut().push_host_pollable(pollable)?) diff --git a/crates/wasi/src/preview2/stream.rs b/crates/wasi/src/preview2/stream.rs index 54c4dda33bdc..1df5de0346c5 100644 --- a/crates/wasi/src/preview2/stream.rs +++ b/crates/wasi/src/preview2/stream.rs @@ -1,3 +1,4 @@ +use crate::preview2::filesystem::{FileInputStream, FileOutputStream}; use crate::preview2::{Table, TableError}; use anyhow::Error; use bytes::Bytes; @@ -89,24 +90,75 @@ pub trait HostOutputStream: Send + Sync { async fn ready(&mut self) -> Result<(), Error>; } -#[async_trait::async_trait] -pub(crate) trait BlockingInputStream: Send + Sync { - async fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error>; -} - pub(crate) enum InternalInputStream { Host(Box), - Blocking(Box), -} - -#[async_trait::async_trait] -pub(crate) trait BlockingOutputStream: Send + Sync { - async fn write(&mut self, bytes: Bytes) -> Result<(usize, StreamState), Error>; + File(FileInputStream), } pub(crate) enum InternalOutputStream { Host(Box), - Blocking(Box), + File(FileOutputStream), +} + +pub(crate) trait InternalTableStreamExt { + fn push_internal_input_stream( + &mut self, + istream: InternalInputStream, + ) -> Result; + fn get_internal_input_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut InternalInputStream, TableError>; + fn delete_internal_input_stream(&mut self, fd: u32) -> Result; + + fn push_internal_output_stream( + &mut self, + ostream: InternalOutputStream, + ) -> Result; + fn get_internal_output_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut InternalOutputStream, TableError>; + fn delete_internal_output_stream( + &mut self, + fd: u32, + ) -> Result; +} +impl InternalTableStreamExt for Table { + fn push_internal_input_stream( + &mut self, + istream: InternalInputStream, + ) -> Result { + self.push(Box::new(istream)) + } + fn get_internal_input_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut InternalInputStream, TableError> { + self.get_mut(fd) + } + fn delete_internal_input_stream(&mut self, fd: u32) -> Result { + self.delete(fd) + } + + fn push_internal_output_stream( + &mut self, + ostream: InternalOutputStream, + ) -> Result { + self.push(Box::new(ostream)) + } + fn get_internal_output_stream_mut( + &mut self, + fd: u32, + ) -> Result<&mut InternalOutputStream, TableError> { + self.get_mut(fd) + } + fn delete_internal_output_stream( + &mut self, + fd: u32, + ) -> Result { + self.delete(fd) + } } /// Extension trait for managing [`HostInputStream`]s and [`HostOutputStream`]s in the [`Table`]. @@ -129,10 +181,10 @@ pub trait TableStreamExt { } impl TableStreamExt for Table { fn push_input_stream(&mut self, istream: Box) -> Result { - self.push(Box::new(InternalInputStream::Host(istream))) + self.push_internal_input_stream(InternalInputStream::Host(istream)) } fn get_input_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostInputStream, TableError> { - match self.get_mut::(fd)? { + match self.get_internal_input_stream_mut(fd)? { InternalInputStream::Host(ref mut h) => Ok(h.as_mut()), _ => Err(TableError::WrongType), } @@ -155,10 +207,10 @@ impl TableStreamExt for Table { &mut self, ostream: Box, ) -> Result { - self.push(Box::new(InternalOutputStream::Host(ostream))) + self.push_internal_output_stream(InternalOutputStream::Host(ostream)) } fn get_output_stream_mut(&mut self, fd: u32) -> Result<&mut dyn HostOutputStream, TableError> { - match self.get_mut::(fd)? { + match self.get_internal_output_stream_mut(fd)? { InternalOutputStream::Host(ref mut h) => Ok(h.as_mut()), _ => Err(TableError::WrongType), } From 90d87c3257ce70de74fe1cefc188f0defe1bf83b Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 12:55:50 -0700 Subject: [PATCH 100/118] fixes --- crates/wasi/src/preview2/mod.rs | 11 ---------- crates/wasi/src/preview2/preview2/io.rs | 28 +++++++++++++++++-------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 0b79f3f27f30..e433780d8336 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -180,14 +180,3 @@ pub(crate) fn block_on(f: F) -> F::Output { } } } - -pub(crate) fn block_in_place(f: F) -> R -where - F: FnOnce() -> R, -{ - if tokio::runtime::Handle::try_current().is_ok() { - tokio::task::block_in_place(f) - } else { - f() - } -} diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 411779455f5b..36bbac47a3df 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -283,12 +283,17 @@ impl streams::Host for T { let pollable = match self.table_mut().get_internal_input_stream_mut(stream)? { InternalInputStream::Host(_) => { fn input_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + // FIXME: This downcast and match should be guaranteed by the checks above, + // however, the table element at index could be changed which would make this + // panic! This is a known problem with referring to other resources in the + // table which must be fixed. let stream = stream - .downcast_mut::>() - // Should be impossible because we made sure this will downcast to a - // HostImputStream with table check above. - .expect("downcast to HostInputStream failed"); - stream.ready() + .downcast_mut::() + .expect("downcast to InternalInputStream failed"); + match *stream { + InternalInputStream::Host(ref mut hs) => hs.ready(), + _ => unreachable!(), + } } HostPollable::TableEntry { @@ -313,12 +318,17 @@ impl streams::Host for T { let pollable = match self.table_mut().get_internal_output_stream_mut(stream)? { InternalOutputStream::Host(_) => { fn output_stream_ready<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + // FIXME: This downcast and match should be guaranteed by the checks above, + // however, the table element at index could be changed which would make this + // panic! This is a known problem with referring to other resources in the + // table which must be fixed. let stream = stream - .downcast_mut::>() - // Should be impossible because we made sure this will downcast to a - // HostOutputStream with table check above. + .downcast_mut::() .expect("downcast to HostOutputStream failed"); - stream.ready() + match *stream { + InternalOutputStream::Host(ref mut hs) => hs.ready(), + _ => unreachable!(), + } } HostPollable::TableEntry { From 6d39ecfd0c8744618f3a43011e352d8d19c01fc1 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 14:35:48 -0700 Subject: [PATCH 101/118] satisfy cargo doc --- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/poll.rs | 2 +- crates/wasi/src/preview2/table.rs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index e433780d8336..2cd9667d40f4 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -34,7 +34,7 @@ pub use self::clocks::{HostMonotonicClock, HostWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; pub use self::error::I32Exit; pub use self::filesystem::{DirPerms, FilePerms}; -pub use self::poll::{HostPollable, TablePollableExt}; +pub use self::poll::{ClosureFuture, HostPollable, MakeFuture, PollableFuture, TablePollableExt}; pub use self::random::{thread_rng, Deterministic}; pub use self::stream::{HostInputStream, HostOutputStream, StreamState, TableStreamExt}; pub use self::table::{Table, TableError}; diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index cbb48b3d46c8..d9b6f15d4341 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -27,7 +27,7 @@ pub enum HostPollable { /// FIXME: we currently aren't tracking the lifetime of the resource along /// with this entry, which means that this index could be occupied by something /// unrelated by the time we poll it again. This is a crash vector, because - /// the [MakeFuture] would panic if the type of the index has changed, and + /// the [`MakeFuture`] would panic if the type of the index has changed, and /// would yield undefined behavior otherwise. We'll likely fix this by making /// the parent resources of a pollable clean up their pollable entries when /// they are destroyed (e.g. the HostInputStream would track the pollables it diff --git a/crates/wasi/src/preview2/table.rs b/crates/wasi/src/preview2/table.rs index 675850d7df4b..814fdf3f15c4 100644 --- a/crates/wasi/src/preview2/table.rs +++ b/crates/wasi/src/preview2/table.rs @@ -87,8 +87,9 @@ impl Table { } } - /// Get an [`OccupiedEntry`] corresponding to a table entry, if it exists. This allows you to - /// remove or replace the entry based on its contents. + /// Get an [`std::collections::hash_map::OccupiedEntry`] corresponding to + /// a table entry, if it exists. This allows you to remove or replace the + /// entry based on its contents. pub fn entry( &mut self, key: u32, From 8fe07083da48d88d197707243a1d46bcb3595134 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 14:39:40 -0700 Subject: [PATCH 102/118] cargo vet: dep upgrades taken care of by imports from mozilla --- supply-chain/imports.lock | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 45858cca9857..b7590b854351 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -1936,6 +1936,24 @@ criteria = "safe-to-deploy" delta = "2.2.1 -> 2.3.2" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.bytes]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.2.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bytes]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.2.1 -> 1.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bytes]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.3.0 -> 1.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.crypto-common]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -1992,6 +2010,12 @@ version = "1.0.7" notes = "Simple hasher implementation with no unsafe code." aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.futures-io]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.fxhash]] who = "Bobby Holley " criteria = "safe-to-deploy" From d21b55668988c90ff18dd1ff36c5695d5897984e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 15:08:24 -0700 Subject: [PATCH 103/118] unix stdio: eliminate block_in_place --- crates/wasi/src/preview2/stdio/unix.rs | 63 ++++++++++++++------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index d22aa26beca7..e45a9d66d512 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -1,30 +1,46 @@ -use crate::preview2::{pipe::AsyncReadStream, StreamState}; +use crate::preview2::{pipe::AsyncReadStream, HostInputStream, StreamState}; use anyhow::Error; use bytes::Bytes; +use futures::ready; +use std::future::Future; +use std::io::{self, Read}; +use std::pin::Pin; +use std::task::{Context, Poll}; use tokio::io::unix::AsyncFd; +use tokio::io::{AsyncRead, ReadBuf}; // wasmtime cant use std::sync::OnceLock yet because of a llvm regression in // 1.70. when 1.71 is released, we can switch to using std here. use once_cell::sync::OnceCell as OnceLock; -// FIXME: we might be able to eliminate this, and block_in_place as well, -// if we write ready() with an impl Future that takes and releases a std::sync::mutex -// as part of every poll() invocation. It isnt critical that ready hold the -// lock for the duration of the polling - using stdin from multiple contexts -// is already bogus in terms of application functionality, we are just trying to -// make the implementation typecheck. -// We use a tokio Mutex because, in ready(), the mutex needs to be held -// across an await. -use tokio::sync::Mutex; +use std::sync::Mutex; // We need a single global instance of the AsyncFd because creating // this instance registers the process's stdin fd with epoll, which will // return an error if an fd is registered more than once. -type GlobalStdin = Mutex; +struct GlobalStdin(Mutex); static STDIN: OnceLock = OnceLock::new(); -fn create() -> anyhow::Result { - Ok(Mutex::new(AsyncReadStream::new(InnerStdin::new()?))) +impl GlobalStdin { + fn new() -> anyhow::Result { + Ok(Self(Mutex::new(AsyncReadStream::new(InnerStdin::new()?)))) + } + fn read(&self, size: usize) -> Result<(Bytes, StreamState), Error> { + HostInputStream::read(&mut *self.0.lock().unwrap(), size) + } + fn ready<'a>(&'a self) -> impl Future> + 'a { + struct Ready<'a>(&'a GlobalStdin); + impl<'a> Future for Ready<'a> { + type Output = Result<(), Error>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let mut locked = self.as_mut().0 .0.lock().unwrap(); + let fut = locked.ready(); + tokio::pin!(fut); + fut.poll(cx) + } + } + Ready(self) + } } pub struct Stdin; @@ -33,11 +49,12 @@ impl Stdin { // Creation must be running in a tokio context to succeed. match tokio::runtime::Handle::try_current() { Ok(_) => STDIN.get_or_init(|| { - create().expect("creating AsyncFd for stdin in existing tokio context") + GlobalStdin::new().expect("creating AsyncFd for stdin in existing tokio context") }), Err(_) => STDIN.get_or_init(|| { crate::preview2::block_on(async { - create().expect("creating AsyncFd for stdin in internal tokio context") + GlobalStdin::new() + .expect("creating AsyncFd for stdin in internal tokio context") }) }), } @@ -51,17 +68,11 @@ pub fn stdin() -> Stdin { #[async_trait::async_trait] impl crate::preview2::HostInputStream for Stdin { fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - let r = move || Self::get_global().blocking_lock().read(size); - // If we are currently in a tokio context, blocking_lock will panic unless inside a - // block_in_place: - match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::block_in_place(r), - Err(_) => r(), - } + Self::get_global().read(size) } async fn ready(&mut self) -> Result<(), Error> { - Self::get_global().lock().await.ready().await + Self::get_global().ready().await } } @@ -88,12 +99,6 @@ impl InnerStdin { } } -use futures::ready; -use std::io::{self, Read}; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, ReadBuf}; - impl AsyncRead for InnerStdin { fn poll_read( mut self: Pin<&mut Self>, From f6c28ef2a5e2d509f81a8254c113f00f37137d2c Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 15:08:54 -0700 Subject: [PATCH 104/118] replace private in_tokio with spawn, since its only used for spawning --- crates/wasi/src/preview2/mod.rs | 10 ++-- crates/wasi/src/preview2/pipe.rs | 82 +++++++++++++++----------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 2cd9667d40f4..ef8c81a4132c 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -156,12 +156,16 @@ static RUNTIME: once_cell::sync::Lazy = once_cell::sync .unwrap() }); -pub(crate) fn in_tokio G>(f: F) -> G { +pub(crate) fn spawn(f: F) -> tokio::task::JoinHandle +where + F: std::future::Future + Send + 'static, + G: Send + 'static, +{ match tokio::runtime::Handle::try_current() { - Ok(_) => f(), + Ok(_) => tokio::task::spawn(f), Err(_) => { let _enter = RUNTIME.enter(); - RUNTIME.block_on(async { f() }) + tokio::task::spawn(f) } } } diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 32c530332b63..f378f187326b 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -113,24 +113,22 @@ impl AsyncReadStream { /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`]. pub fn new(mut reader: T) -> Self { let (sender, receiver) = tokio::sync::mpsc::channel(1); - crate::preview2::in_tokio(|| { - tokio::spawn(async move { - loop { - use tokio::io::AsyncReadExt; - let mut buf = bytes::BytesMut::with_capacity(4096); - let sent = match reader.read_buf(&mut buf).await { - Ok(nbytes) if nbytes == 0 => { - sender.send(Ok((Bytes::new(), StreamState::Closed))).await - } - Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, - Err(e) => sender.send(Err(e)).await, - }; - if sent.is_err() { - // no more receiver - stop trying to read - break; + crate::preview2::spawn(async move { + loop { + use tokio::io::AsyncReadExt; + let mut buf = bytes::BytesMut::with_capacity(4096); + let sent = match reader.read_buf(&mut buf).await { + Ok(nbytes) if nbytes == 0 => { + sender.send(Ok((Bytes::new(), StreamState::Closed))).await } + Ok(_) => sender.send(Ok((buf.freeze(), StreamState::Open))).await, + Err(e) => sender.send(Err(e)).await, + }; + if sent.is_err() { + // no more receiver - stop trying to read + break; } - }) + } }); AsyncReadStream { state: StreamState::Open, @@ -228,40 +226,38 @@ impl AsyncWriteStream { let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); let (result_sender, result_receiver) = tokio::sync::mpsc::channel(1); - crate::preview2::in_tokio(|| { - tokio::spawn(async move { - 'outer: loop { - use tokio::io::AsyncWriteExt; - match receiver.recv().await { - Some(mut bytes) => { - while !bytes.is_empty() { - match writer.write_buf(&mut bytes).await { - Ok(0) => { - let _ = result_sender.send(Ok(StreamState::Closed)).await; - break 'outer; - } - Ok(_) => { - if bytes.is_empty() { - match result_sender.send(Ok(StreamState::Open)).await { - Ok(_) => break, - Err(_) => break 'outer, - } + crate::preview2::spawn(async move { + 'outer: loop { + use tokio::io::AsyncWriteExt; + match receiver.recv().await { + Some(mut bytes) => { + while !bytes.is_empty() { + match writer.write_buf(&mut bytes).await { + Ok(0) => { + let _ = result_sender.send(Ok(StreamState::Closed)).await; + break 'outer; + } + Ok(_) => { + if bytes.is_empty() { + match result_sender.send(Ok(StreamState::Open)).await { + Ok(_) => break, + Err(_) => break 'outer, } - continue; - } - Err(e) => { - let _ = result_sender.send(Err(e)).await; - break 'outer; } + continue; + } + Err(e) => { + let _ = result_sender.send(Err(e)).await; + break 'outer; } } } - - // The other side of the channel hung up, the task can exit now - None => break 'outer, } + + // The other side of the channel hung up, the task can exit now + None => break 'outer, } - }) + } }); AsyncWriteStream { From db4f8829c32b3ce629f6388668d6723fa37f1778 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 16:36:07 -0700 Subject: [PATCH 105/118] comments --- crates/wasi/src/preview2/stdio/unix.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index e45a9d66d512..4469d4e05b70 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -29,6 +29,14 @@ impl GlobalStdin { HostInputStream::read(&mut *self.0.lock().unwrap(), size) } fn ready<'a>(&'a self) -> impl Future> + 'a { + // Custom Future impl takes the std mutex in each invocation of poll. + // Required so we don't have to use a tokio mutex, which we can't take from + // inside a sync context in Self::read. + // + // Taking the lock, creating a fresh ready() future, polling it once, and + // then releasing the lock is acceptable here because the ready() future + // is only ever going to await on a single channel recv, plus some management + // of a state machine (for buffering). struct Ready<'a>(&'a GlobalStdin); impl<'a> Future for Ready<'a> { type Output = Result<(), Error>; From f60b57f688406548e6fd16f29c66e3cb9cab5932 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 16:36:15 -0700 Subject: [PATCH 106/118] worker thread stdin implementation can be tested on linux, i guess and start outlining a test plan --- crates/wasi/src/preview2/stdio.rs | 51 +++++++- crates/wasi/src/preview2/stdio/windows.rs | 90 -------------- .../src/preview2/stdio/worker_thread_stdin.rs | 117 ++++++++++++++++++ 3 files changed, 165 insertions(+), 93 deletions(-) delete mode 100644 crates/wasi/src/preview2/stdio/windows.rs create mode 100644 crates/wasi/src/preview2/stdio/worker_thread_stdin.rs diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs index b47ba772a1a4..8386f6611ae8 100644 --- a/crates/wasi/src/preview2/stdio.rs +++ b/crates/wasi/src/preview2/stdio.rs @@ -5,10 +5,10 @@ mod unix; #[cfg(unix)] pub use self::unix::{stdin, Stdin}; +#[allow(dead_code)] +mod worker_thread_stdin; #[cfg(windows)] -mod windows; -#[cfg(windows)] -pub use self::windows::{stdin, Stdin}; +pub use self::worker_thread_stdin::{stdin, Stdin}; pub type Stdout = AsyncWriteStream; @@ -20,3 +20,48 @@ pub type Stderr = AsyncWriteStream; pub fn stderr() -> Stderr { AsyncWriteStream::new(tokio::io::stderr()) } + +#[cfg(all(unix, test))] +mod test { + // This could even be parameterized somehow to use the worker thread stdin vs the asyncfd + // stdin. + #[test] + fn test_stdin_by_forking() { + // Make pipe for emulating stdin. + // Make pipe for getting results. + // Fork. + // When child: + // close stdin fd. + // use dup2 to turn the pipe recv end into the stdin fd. + // in a tokio runtime: + // let stdin = super::stdin(); + // // Make sure the initial state is that stdin is not ready: + // if timeout(stdin.ready().await).is_timeout() { + // send "start\n" on result pipe. + // } + // loop { + // match timeout(stdin.ready().await) { + // Ok => { + // let bytes = stdin.read(); + // if bytes == ending sentinel: + // exit + // if bytes == some other sentinel: + // return and go back to the thing where we start the tokio runtime, + // testing that when creating a new super::stdin() it works correctly + // send "got: {bytes:?}\n" on result pipe. + // } + // Err => { + // send "timed out\n" on result pipe. + // } + // } + // } + // When parent: + // wait to recv "start\n" on result pipe (or the child process exits) + // send some bytes to child stdin. + // make sure we get back "got {bytes:?}" on result pipe (or the child process exits) + // sleep for a while. + // make sure we get back "timed out" on result pipe (or the child process exits) + // send some bytes again. and etc. + // + } +} diff --git a/crates/wasi/src/preview2/stdio/windows.rs b/crates/wasi/src/preview2/stdio/windows.rs deleted file mode 100644 index fc39afdf7682..000000000000 --- a/crates/wasi/src/preview2/stdio/windows.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::preview2::{HostInputStream, StreamState}; -use anyhow::{Context, Error}; -use bytes::Bytes; - -// wasmtime cant use std::sync::OnceLock yet because of a llvm regression in -// 1.70. when 1.71 is released, we can switch to using std here. -use once_cell::sync::OnceCell as OnceLock; - -// We use a tokio Mutex because, in ready(), the mutex needs to be held -// across an await. -use tokio::sync::Mutex; - -// We need a single global instance of the AsyncFd because creating -// this instance registers the process's stdin fd with epoll, which will -// return an error if an fd is registered more than once. -struct GlobalStdin { - tx: tokio::sync::mpsc::Sender>>, - // FIXME use a Watch to check for readiness instead of sending a oneshot sender -} -static STDIN: OnceLock> = OnceLock::new(); - -fn create() -> Mutex { - let (tx, mut rx) = - tokio::sync::mpsc::channel::>>(1); - std::thread::spawn(move || { - use std::io::BufRead; - // A client is interested in stdin's readiness - while let Some(msg) = rx.blocking_recv() { - // Fill buf - can we skip this if its - // already filled? - // also, this could block forever and the - // client could give up. in that case, - // another client may want to start waiting - let r = std::io::stdin() - .lock() - .fill_buf() - .map(|_| ()) - .map_err(anyhow::Error::from); - // tell the client stdin is ready for reading - let _ = msg.send(r); - } - }); - - Mutex::new(GlobalStdin { tx }) -} - -pub struct Stdin; -impl Stdin { - fn get_global() -> &'static Mutex { - // Creation must be running in a tokio context to succeed. - match tokio::runtime::Handle::try_current() { - Ok(_) => STDIN.get_or_init(|| create()), - Err(_) => STDIN.get_or_init(|| crate::preview2::block_on(async { create() })), - } - } -} - -pub fn stdin() -> Stdin { - Stdin -} - -#[async_trait::async_trait] -impl HostInputStream for Stdin { - fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { - // use std::io::Read; - // let mut r = move || { - // let nbytes = std::io::stdin().read(buf)?; - // // FIXME handle eof - // Ok((nbytes as u64, StreamState::Open)) - // }; - // // If we are currently in a tokio context, block: - // match tokio::runtime::Handle::try_current() { - // Ok(_) => tokio::task::block_in_place(r), - // Err(_) => r(), - // } - todo!() - } - - async fn ready(&mut self) -> Result<(), Error> { - let (result_tx, rx) = tokio::sync::oneshot::channel::>(); - Self::get_global() - .lock() - .await - .tx - .send(result_tx) - .await // Could hang here if we another wait on this was canceled?? - .context("sending message to worker thread")?; - rx.await.expect("channel is always alive") - } -} diff --git a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs new file mode 100644 index 000000000000..60e3cac8b3a3 --- /dev/null +++ b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs @@ -0,0 +1,117 @@ +use crate::preview2::{HostInputStream, StreamState}; +use anyhow::{Context, Error}; +use bytes::Bytes; +use tokio::sync::{mpsc, oneshot}; + +// wasmtime cant use std::sync::OnceLock yet because of a llvm regression in +// 1.70. when 1.71 is released, we can switch to using std here. +use once_cell::sync::OnceCell as OnceLock; + +use std::sync::Mutex; + +// We need a single global instance of the AsyncFd because creating +// this instance registers the process's stdin fd with epoll, which will +// return an error if an fd is registered more than once. +struct GlobalStdin { + tx: mpsc::Sender>>, + // FIXME use a Watch to check for readiness instead of sending a oneshot sender +} +static STDIN: OnceLock> = OnceLock::new(); + +fn create() -> Mutex { + let (tx, mut rx) = mpsc::channel::>>(1); + std::thread::spawn(move || { + use std::io::BufRead; + // A client is interested in stdin's readiness. + // Don't care about the None case - the GlobalStdin sender on the other + // end of this pipe will live forever, because it lives inside the OnceLock. + while let Some(msg) = rx.blocking_recv() { + // Fill buf - can we skip this if its + // already filled? + // also, this could block forever and the + // client could give up. in that case, + // another client may want to start waiting + let r = std::io::stdin() + .lock() + .fill_buf() + .map(|_| ()) + .map_err(anyhow::Error::from); + // tell the client stdin is ready for reading. + // don't care if the client happens to have died. + let _ = msg.send(r); + } + }); + + Mutex::new(GlobalStdin { tx }) +} + +pub struct Stdin; +impl Stdin { + fn get_global() -> &'static Mutex { + // Creation must be running in a tokio context to succeed. + match tokio::runtime::Handle::try_current() { + Ok(_) => STDIN.get_or_init(|| create()), + Err(_) => STDIN.get_or_init(|| crate::preview2::block_on(async { create() })), + } + } +} + +pub fn stdin() -> Stdin { + Stdin +} + +#[async_trait::async_trait] +impl HostInputStream for Stdin { + fn read(&mut self, size: usize) -> Result<(Bytes, StreamState), Error> { + use std::io::Read; + let mut buf = vec![0; size]; + // FIXME: this is actually blocking. This whole implementation is likely bogus as a result + let nbytes = std::io::stdin().read(&mut buf)?; + buf.truncate(nbytes); + Ok(( + buf.into(), + if nbytes > 0 { + StreamState::Open + } else { + StreamState::Closed + }, + )) + } + + async fn ready(&mut self) -> Result<(), Error> { + use mpsc::error::TrySendError; + use std::future::Future; + use std::pin::Pin; + use std::task::{Context, Poll}; + + // Custom Future impl takes the std mutex in each invocation of poll. + // Required so we don't have to use a tokio mutex, which we can't take from + // inside a sync context in Self::read. + // + // Take the lock, attempt to + struct Send(Option>>); + impl Future for Send { + type Output = anyhow::Result<()>; + fn poll(mut self: Pin<&mut Self>, _: &mut Context) -> Poll { + let locked = Stdin::get_global().lock().unwrap(); + let to_send = self.as_mut().0.take().expect("to_send should be some"); + match locked.tx.try_send(to_send) { + Ok(()) => Poll::Ready(Ok(())), + Err(TrySendError::Full(to_send)) => { + self.as_mut().0.replace(to_send); + Poll::Pending + } + Err(TrySendError::Closed(_)) => { + Poll::Ready(Err(anyhow::anyhow!("channel to GlobalStdin closed"))) + } + } + } + } + + let (result_tx, rx) = oneshot::channel::>(); + Box::pin(Send(Some(result_tx))) + .await + .context("sending message to worker thread")?; + rx.await.expect("channel is always alive") + } +} From 0a4d5000b8fa19d703e6eceaec42aea9d5021fba Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 16:39:38 -0700 Subject: [PATCH 107/118] eliminate tokio boilerplate - no longer using tokios lock --- crates/wasi/src/preview2/mod.rs | 2 -- crates/wasi/src/preview2/stdio/worker_thread_stdin.rs | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index ef8c81a4132c..93a6ecfb0a62 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -170,8 +170,6 @@ where } } -// FIXME: this can maybe be written in terms of `in_tokio` but i have broken my tools with my -// tools right now and cant trust my tests pub(crate) fn block_on(f: F) -> F::Output { match tokio::runtime::Handle::try_current() { Ok(h) => { diff --git a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs index 60e3cac8b3a3..34bc8b8e693d 100644 --- a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs +++ b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs @@ -48,11 +48,7 @@ fn create() -> Mutex { pub struct Stdin; impl Stdin { fn get_global() -> &'static Mutex { - // Creation must be running in a tokio context to succeed. - match tokio::runtime::Handle::try_current() { - Ok(_) => STDIN.get_or_init(|| create()), - Err(_) => STDIN.get_or_init(|| crate::preview2::block_on(async { create() })), - } + STDIN.get_or_init(|| create()) } } From 834430f8a03eb0e4bf3fd6b088541f1c6cedccd9 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 16:42:03 -0700 Subject: [PATCH 108/118] rename our private block_on to in_tokio --- crates/wasi/src/preview2/mod.rs | 2 +- crates/wasi/src/preview2/poll.rs | 6 +- .../src/preview2/preview2/filesystem/sync.rs | 72 +++++++++---------- crates/wasi/src/preview2/preview2/io.rs | 32 ++++----- crates/wasi/src/preview2/stdio/unix.rs | 2 +- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 93a6ecfb0a62..28271d26070a 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -170,7 +170,7 @@ where } } -pub(crate) fn block_on(f: F) -> F::Output { +pub(crate) fn in_tokio(f: F) -> F::Output { match tokio::runtime::Handle::try_current() { Ok(h) => { let _enter = h.enter(); diff --git a/crates/wasi/src/preview2/poll.rs b/crates/wasi/src/preview2/poll.rs index d9b6f15d4341..883edf63d138 100644 --- a/crates/wasi/src/preview2/poll.rs +++ b/crates/wasi/src/preview2/poll.rs @@ -141,17 +141,17 @@ pub mod sync { use crate::preview2::{ bindings::poll::poll::Host as AsyncHost, bindings::sync_io::poll::poll::{self, Pollable}, - block_on, WasiView, + in_tokio, WasiView, }; use anyhow::Result; impl poll::Host for T { fn drop_pollable(&mut self, pollable: Pollable) -> Result<()> { - block_on(async { AsyncHost::drop_pollable(self, pollable).await }) + in_tokio(async { AsyncHost::drop_pollable(self, pollable).await }) } fn poll_oneoff(&mut self, pollables: Vec) -> Result> { - block_on(async { AsyncHost::poll_oneoff(self, pollables).await }) + in_tokio(async { AsyncHost::poll_oneoff(self, pollables).await }) } } } diff --git a/crates/wasi/src/preview2/preview2/filesystem/sync.rs b/crates/wasi/src/preview2/preview2/filesystem/sync.rs index 7e0909ac7e5e..64f5973b32ef 100644 --- a/crates/wasi/src/preview2/preview2/filesystem/sync.rs +++ b/crates/wasi/src/preview2/preview2/filesystem/sync.rs @@ -1,7 +1,7 @@ use crate::preview2::bindings::filesystem::filesystem as async_filesystem; use crate::preview2::bindings::sync_io::filesystem::filesystem as sync_filesystem; use crate::preview2::bindings::sync_io::io::streams; -use crate::preview2::block_on; +use crate::preview2::in_tokio; impl sync_filesystem::Host for T { fn advise( @@ -11,13 +11,13 @@ impl sync_filesystem::Host for T { len: sync_filesystem::Filesize, advice: sync_filesystem::Advice, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::advise(self, fd, offset, len, advice.into()).await })?) } fn sync_data(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::sync_data(self, fd).await })?) } @@ -26,14 +26,14 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result { - Ok(block_on(async { async_filesystem::Host::get_flags(self, fd).await })?.into()) + Ok(in_tokio(async { async_filesystem::Host::get_flags(self, fd).await })?.into()) } fn get_type( &mut self, fd: sync_filesystem::Descriptor, ) -> Result { - Ok(block_on(async { async_filesystem::Host::get_type(self, fd).await })?.into()) + Ok(in_tokio(async { async_filesystem::Host::get_type(self, fd).await })?.into()) } fn set_size( @@ -41,7 +41,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, size: sync_filesystem::Filesize, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::set_size(self, fd, size).await })?) } @@ -52,7 +52,7 @@ impl sync_filesystem::Host for T { atim: sync_filesystem::NewTimestamp, mtim: sync_filesystem::NewTimestamp, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::set_times(self, fd, atim.into(), mtim.into()).await })?) } @@ -63,7 +63,7 @@ impl sync_filesystem::Host for T { len: sync_filesystem::Filesize, offset: sync_filesystem::Filesize, ) -> Result<(Vec, bool), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::read(self, fd, len, offset).await })?) } @@ -74,7 +74,7 @@ impl sync_filesystem::Host for T { buf: Vec, offset: sync_filesystem::Filesize, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::write(self, fd, buf, offset).await })?) } @@ -83,7 +83,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::read_directory(self, fd).await })?) } @@ -93,7 +93,7 @@ impl sync_filesystem::Host for T { stream: sync_filesystem::DirectoryEntryStream, ) -> Result, sync_filesystem::Error> { Ok( - block_on(async { async_filesystem::Host::read_directory_entry(self, stream).await })? + in_tokio(async { async_filesystem::Host::read_directory_entry(self, stream).await })? .map(|e| e.into()), ) } @@ -102,13 +102,13 @@ impl sync_filesystem::Host for T { &mut self, stream: sync_filesystem::DirectoryEntryStream, ) -> anyhow::Result<()> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::drop_directory_entry_stream(self, stream).await })?) } fn sync(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::sync(self, fd).await })?) } @@ -118,7 +118,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::create_directory_at(self, fd, path).await })?) } @@ -127,7 +127,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result { - Ok(block_on(async { async_filesystem::Host::stat(self, fd).await })?.into()) + Ok(in_tokio(async { async_filesystem::Host::stat(self, fd).await })?.into()) } fn stat_at( @@ -136,7 +136,7 @@ impl sync_filesystem::Host for T { path_flags: sync_filesystem::PathFlags, path: String, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::stat_at(self, fd, path_flags.into(), path).await })? .into()) @@ -150,7 +150,7 @@ impl sync_filesystem::Host for T { atim: sync_filesystem::NewTimestamp, mtim: sync_filesystem::NewTimestamp, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::set_times_at( self, fd, @@ -172,7 +172,7 @@ impl sync_filesystem::Host for T { new_descriptor: sync_filesystem::Descriptor, new_path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::link_at( self, fd, @@ -194,7 +194,7 @@ impl sync_filesystem::Host for T { flags: sync_filesystem::DescriptorFlags, mode: sync_filesystem::Modes, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::open_at( self, fd, @@ -209,7 +209,7 @@ impl sync_filesystem::Host for T { } fn drop_descriptor(&mut self, fd: sync_filesystem::Descriptor) -> anyhow::Result<()> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::drop_descriptor(self, fd).await })?) } @@ -219,7 +219,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, path: String, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::readlink_at(self, fd, path).await })?) } @@ -229,7 +229,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::remove_directory_at(self, fd, path).await })?) } @@ -241,7 +241,7 @@ impl sync_filesystem::Host for T { new_fd: sync_filesystem::Descriptor, new_path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::rename_at(self, fd, old_path, new_fd, new_path).await })?) } @@ -252,7 +252,7 @@ impl sync_filesystem::Host for T { src_path: String, dest_path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::symlink_at(self, fd, src_path, dest_path).await })?) } @@ -262,7 +262,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, path: String, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::unlink_file_at(self, fd, path).await })?) } @@ -274,7 +274,7 @@ impl sync_filesystem::Host for T { path: String, access: sync_filesystem::AccessType, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::access_at(self, fd, path_flags.into(), path, access.into()) .await })?) @@ -287,7 +287,7 @@ impl sync_filesystem::Host for T { path: String, mode: sync_filesystem::Modes, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::change_file_permissions_at( self, fd, @@ -306,7 +306,7 @@ impl sync_filesystem::Host for T { path: String, mode: sync_filesystem::Modes, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::change_directory_permissions_at( self, fd, @@ -322,7 +322,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::lock_shared(self, fd).await })?) } @@ -331,7 +331,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::lock_exclusive(self, fd).await })?) } @@ -340,7 +340,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::try_lock_shared(self, fd).await })?) } @@ -349,13 +349,13 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::try_lock_exclusive(self, fd).await })?) } fn unlock(&mut self, fd: sync_filesystem::Descriptor) -> Result<(), sync_filesystem::Error> { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::unlock(self, fd).await })?) } @@ -365,7 +365,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, offset: sync_filesystem::Filesize, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::read_via_stream(self, fd, offset).await })?) } @@ -375,7 +375,7 @@ impl sync_filesystem::Host for T { fd: sync_filesystem::Descriptor, offset: sync_filesystem::Filesize, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::write_via_stream(self, fd, offset).await })?) } @@ -384,7 +384,7 @@ impl sync_filesystem::Host for T { &mut self, fd: sync_filesystem::Descriptor, ) -> Result { - Ok(block_on(async { + Ok(in_tokio(async { async_filesystem::Host::append_via_stream(self, fd).await })?) } diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 36bbac47a3df..44a3cb907c58 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -350,7 +350,7 @@ pub mod sync { bindings::io::streams::{Host as AsyncHost, StreamStatus as AsyncStreamStatus}, bindings::sync_io::io::streams::{self, InputStream, OutputStream}, bindings::sync_io::poll::poll::Pollable, - block_on, WasiView, + in_tokio, WasiView, }; impl From for streams::StreamStatus { @@ -364,11 +364,11 @@ pub mod sync { impl streams::Host for T { fn drop_input_stream(&mut self, stream: InputStream) -> anyhow::Result<()> { - block_on(async { AsyncHost::drop_input_stream(self, stream).await }) + in_tokio(async { AsyncHost::drop_input_stream(self, stream).await }) } fn drop_output_stream(&mut self, stream: OutputStream) -> anyhow::Result<()> { - block_on(async { AsyncHost::drop_output_stream(self, stream).await }) + in_tokio(async { AsyncHost::drop_output_stream(self, stream).await }) } fn read( @@ -376,7 +376,7 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::read(self, stream, len).await }) + in_tokio(async { AsyncHost::read(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -386,7 +386,7 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(Vec, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::blocking_read(self, stream, len).await }) + in_tokio(async { AsyncHost::blocking_read(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -396,7 +396,7 @@ pub mod sync { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::write(self, stream, bytes).await }) + in_tokio(async { AsyncHost::write(self, stream, bytes).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -406,7 +406,7 @@ pub mod sync { stream: OutputStream, bytes: Vec, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::write(self, stream, bytes).await }) + in_tokio(async { AsyncHost::write(self, stream, bytes).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -416,7 +416,7 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::skip(self, stream, len).await }) + in_tokio(async { AsyncHost::skip(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -426,7 +426,7 @@ pub mod sync { stream: InputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::blocking_skip(self, stream, len).await }) + in_tokio(async { AsyncHost::blocking_skip(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -436,7 +436,7 @@ pub mod sync { stream: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::write_zeroes(self, stream, len).await }) + in_tokio(async { AsyncHost::write_zeroes(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -446,7 +446,7 @@ pub mod sync { stream: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) + in_tokio(async { AsyncHost::blocking_write_zeroes(self, stream, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -457,7 +457,7 @@ pub mod sync { dst: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::splice(self, src, dst, len).await }) + in_tokio(async { AsyncHost::splice(self, src, dst, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -468,7 +468,7 @@ pub mod sync { dst: OutputStream, len: u64, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::blocking_splice(self, src, dst, len).await }) + in_tokio(async { AsyncHost::blocking_splice(self, src, dst, len).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } @@ -478,17 +478,17 @@ pub mod sync { src: InputStream, dst: OutputStream, ) -> Result<(u64, streams::StreamStatus), streams::Error> { - block_on(async { AsyncHost::forward(self, src, dst).await }) + in_tokio(async { AsyncHost::forward(self, src, dst).await }) .map(|(a, b)| (a, b.into())) .map_err(streams::Error::from) } fn subscribe_to_input_stream(&mut self, stream: InputStream) -> anyhow::Result { - block_on(async { AsyncHost::subscribe_to_input_stream(self, stream).await }) + in_tokio(async { AsyncHost::subscribe_to_input_stream(self, stream).await }) } fn subscribe_to_output_stream(&mut self, stream: OutputStream) -> anyhow::Result { - block_on(async { AsyncHost::subscribe_to_output_stream(self, stream).await }) + in_tokio(async { AsyncHost::subscribe_to_output_stream(self, stream).await }) } } } diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index 4469d4e05b70..3888b5cdf96d 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -60,7 +60,7 @@ impl Stdin { GlobalStdin::new().expect("creating AsyncFd for stdin in existing tokio context") }), Err(_) => STDIN.get_or_init(|| { - crate::preview2::block_on(async { + crate::preview2::in_tokio(async { GlobalStdin::new() .expect("creating AsyncFd for stdin in internal tokio context") }) From 9d9d3f541906d801d96d7ba86b5d1fb1bac73a02 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 20 Jul 2023 16:45:40 -0700 Subject: [PATCH 109/118] fill in missing file input skip --- crates/wasi/src/preview2/filesystem.rs | 14 ++++++++++++++ crates/wasi/src/preview2/preview2/io.rs | 10 ++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index f519d7011ae5..d13ee68e2dae 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -136,6 +136,20 @@ impl FileInputStream { self.position += n as u64; Ok((buf.freeze(), state)) } + + pub async fn skip(&mut self, nelem: usize) -> anyhow::Result<(usize, StreamState)> { + let mut nread = 0; + let mut state = StreamState::Open; + + let (bs, read_state) = self.read(nelem).await?; + // TODO: handle the case where `bs.len()` is less than `nelem` + nread += bs.len(); + if read_state.is_closed() { + state = read_state; + } + + Ok((nread, state)) + } } pub(crate) fn read_result( diff --git a/crates/wasi/src/preview2/preview2/io.rs b/crates/wasi/src/preview2/preview2/io.rs index 44a3cb907c58..ed03fa9e77b0 100644 --- a/crates/wasi/src/preview2/preview2/io.rs +++ b/crates/wasi/src/preview2/preview2/io.rs @@ -149,7 +149,10 @@ impl streams::Host for T { Ok((bytes_skipped as u64, state.into())) } - InternalInputStream::File(_) => todo!(), + InternalInputStream::File(s) => { + let (bytes_skipped, state) = FileInputStream::skip(s, len as usize).await?; + Ok((bytes_skipped as u64, state.into())) + } } } @@ -166,7 +169,10 @@ impl streams::Host for T { Ok((bytes_skipped as u64, state.into())) } - InternalInputStream::File(_) => todo!(), + InternalInputStream::File(s) => { + let (bytes_skipped, state) = FileInputStream::skip(s, len as usize).await?; + Ok((bytes_skipped as u64, state.into())) + } } } From be9a4b6c1da24236815339d917fb26b5908c4bc7 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 13:10:52 -0700 Subject: [PATCH 110/118] code review: fix MemoryInputPipe. Closed status is always available immediately. --- crates/wasi/src/preview2/pipe.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index f378f187326b..0776815ad1f0 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -47,11 +47,7 @@ impl HostInputStream for MemoryInputPipe { Ok((dest.freeze(), state)) } async fn ready(&mut self) -> Result<(), Error> { - if !self.is_empty() { - Ok(()) - } else { - futures::future::pending().await - } + Ok(()) } } From a3e31b33f2006f9209a2bf62ca746302b801e58e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 13:11:40 -0700 Subject: [PATCH 111/118] code review: empty input stream is not essential, closed input stream is a better fi for stdin --- crates/wasi/src/preview2/ctx.rs | 2 +- crates/wasi/src/preview2/pipe.rs | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 131a2d9cce13..e2d05ff4bb66 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -36,7 +36,7 @@ impl WasiCtxBuilder { let insecure_random_seed = cap_rand::thread_rng(cap_rand::ambient_authority()).gen::(); Self { - stdin: Box::new(pipe::EmptyInputStream), + stdin: Box::new(pipe::ClosedInputStream), stdout: Box::new(pipe::SinkOutputStream), stderr: Box::new(pipe::SinkOutputStream), env: Vec::new(), diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 0776815ad1f0..0c74444daa7e 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -351,21 +351,6 @@ impl HostOutputStream for AsyncWriteStream { } } -/// A stream that is never able to provide input. It stays open forever, but is never ready for -/// reading. -pub struct EmptyInputStream; - -#[async_trait::async_trait] -impl HostInputStream for EmptyInputStream { - fn read(&mut self, _size: usize) -> Result<(Bytes, StreamState), Error> { - Ok((Bytes::new(), StreamState::Open)) - } - - async fn ready(&mut self) -> Result<(), Error> { - futures::future::pending().await - } -} - /// An output stream that consumes all input written to it, and is always ready. pub struct SinkOutputStream; From 3c2f4137ec5b6843951ed880ce5c58c868b0b708 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 13:13:57 -0700 Subject: [PATCH 112/118] code review: unreachable --- crates/wasi/src/preview2/pipe.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index 0c74444daa7e..e720ef93dbc5 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -274,7 +274,9 @@ impl AsyncWriteStream { self.state = Some(WriteState::Pending); Ok((len, StreamState::Open)) } - Err(TrySendError::Full(_)) => Ok((0, StreamState::Open)), + Err(TrySendError::Full(_)) => { + unreachable!("task shouldnt be full when writestate is ready") + } Err(TrySendError::Closed(_)) => unreachable!("task shouldn't die while not closed"), } } From fa6180068c92f4b0ea170aa13bbb74cdb3d1a309 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 14:01:21 -0700 Subject: [PATCH 113/118] turn worker thread (windows) stdin off --- crates/wasi/src/preview2/stdio/worker_thread_stdin.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs index 34bc8b8e693d..353b5c090e62 100644 --- a/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs +++ b/crates/wasi/src/preview2/stdio/worker_thread_stdin.rs @@ -53,7 +53,11 @@ impl Stdin { } pub fn stdin() -> Stdin { - Stdin + // This implementation still needs to be fixed, and we need better test coverage. + // We are deferring that work to a future PR. + // https://github.com/bytecodealliance/wasmtime/pull/6556#issuecomment-1646232646 + panic!("worker-thread based stdin is not yet implemented"); + // Stdin } #[async_trait::async_trait] From 2374cd1340050f78b1c378427e659f2286e163b4 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 14:02:40 -0700 Subject: [PATCH 114/118] expect preview2-based poll_oneoff_stdio to fail on windows --- crates/test-programs/tests/wasi-preview2-components-sync.rs | 2 ++ crates/test-programs/tests/wasi-preview2-components.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/test-programs/tests/wasi-preview2-components-sync.rs b/crates/test-programs/tests/wasi-preview2-components-sync.rs index 80a0ec4aa006..85e8cea10ca4 100644 --- a/crates/test-programs/tests/wasi-preview2-components-sync.rs +++ b/crates/test-programs/tests/wasi-preview2-components-sync.rs @@ -244,6 +244,8 @@ fn path_symlink_trailing_slashes() { fn poll_oneoff_files() { run("poll_oneoff_files", false).unwrap() } + +#[cfg_attr(windows, should_panic)] #[test_log::test] fn poll_oneoff_stdio() { run("poll_oneoff_stdio", true).unwrap() diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index cf59ca5ef931..c0a77443c976 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -248,6 +248,8 @@ async fn path_symlink_trailing_slashes() { async fn poll_oneoff_files() { run("poll_oneoff_files", false).await.unwrap() } + +#[cfg_attr(windows, should_panic)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn poll_oneoff_stdio() { run("poll_oneoff_stdio", true).await.unwrap() From 32fcfa29355c9be62f4a772da89d1ff9120ba0e1 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 14:35:54 -0700 Subject: [PATCH 115/118] command directory_list test: no need to inherit stdin --- crates/test-programs/tests/command.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/test-programs/tests/command.rs b/crates/test-programs/tests/command.rs index 21f020e317e0..9dae33b0a409 100644 --- a/crates/test-programs/tests/command.rs +++ b/crates/test-programs/tests/command.rs @@ -378,7 +378,8 @@ async fn directory_list() -> Result<()> { let mut table = Table::new(); let wasi = WasiCtxBuilder::new() - .inherit_stdio() + .inherit_stdout() + .inherit_stderr() .push_preopened_dir(open_dir, DirPerms::all(), FilePerms::all(), "/") .build(&mut table)?; From 9cd4221aa905180aad0afa674ea9c5eaeb163a45 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 15:09:56 -0700 Subject: [PATCH 116/118] preview1 in preview2: turn off inherit_stdio except for poll_oneoff_stdio --- .../tests/wasi-preview1-host-in-preview2.rs | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs index 812494c297c7..2f0f958c5aba 100644 --- a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs +++ b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs @@ -123,124 +123,126 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { // tests which fail. #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn big_random_buf() { - run("big_random_buf", true).await.unwrap() + run("big_random_buf", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn clock_time_get() { - run("clock_time_get", true).await.unwrap() + run("clock_time_get", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn close_preopen() { - run("close_preopen", true).await.unwrap() + run("close_preopen", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dangling_fd() { - run("dangling_fd", true).await.unwrap() + run("dangling_fd", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dangling_symlink() { - run("dangling_symlink", true).await.unwrap() + run("dangling_symlink", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn directory_seek() { - run("directory_seek", true).await.unwrap() + run("directory_seek", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dir_fd_op_failures() { - run("dir_fd_op_failures", true).await.unwrap() + run("dir_fd_op_failures", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_advise() { - run("fd_advise", true).await.unwrap() + run("fd_advise", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_filestat_get() { - run("fd_filestat_get", true).await.unwrap() + run("fd_filestat_get", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_filestat_set() { - run("fd_filestat_set", true).await.unwrap() + run("fd_filestat_set", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_flags_set() { - run("fd_flags_set", true).await.unwrap() + run("fd_flags_set", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_readdir() { - run("fd_readdir", true).await.unwrap() + run("fd_readdir", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_allocate() { - run("file_allocate", true).await.unwrap() + run("file_allocate", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_pread_pwrite() { - run("file_pread_pwrite", true).await.unwrap() + run("file_pread_pwrite", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_seek_tell() { - run("file_seek_tell", true).await.unwrap() + run("file_seek_tell", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_truncation() { - run("file_truncation", true).await.unwrap() + run("file_truncation", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_unbuffered_write() { - run("file_unbuffered_write", true).await.unwrap() + run("file_unbuffered_write", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[cfg_attr(windows, should_panic)] async fn interesting_paths() { - run("interesting_paths", true).await.unwrap() + run("interesting_paths", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn isatty() { - run("isatty", true).await.unwrap() + run("isatty", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn nofollow_errors() { - run("nofollow_errors", true).await.unwrap() + run("nofollow_errors", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn overwrite_preopen() { - run("overwrite_preopen", true).await.unwrap() + run("overwrite_preopen", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_exists() { - run("path_exists", true).await.unwrap() + run("path_exists", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_filestat() { - run("path_filestat", true).await.unwrap() + run("path_filestat", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_link() { - run("path_link", true).await.unwrap() + run("path_link", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_create_existing() { - run("path_open_create_existing", true).await.unwrap() + run("path_open_create_existing", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_read_write() { - run("path_open_read_write", true).await.unwrap() + run("path_open_read_write", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_dirfd_not_dir() { - run("path_open_dirfd_not_dir", true).await.unwrap() + run("path_open_dirfd_not_dir", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_missing() { - run("path_open_missing", true).await.unwrap() + run("path_open_missing", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_nonblock() { - run("path_open_nonblock", true).await.unwrap() + run("path_open_nonblock", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_rename_dir_trailing_slashes() { - run("path_rename_dir_trailing_slashes", true).await.unwrap() + run("path_rename_dir_trailing_slashes", false) + .await + .unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[should_panic] @@ -251,11 +253,11 @@ async fn path_rename_file_trailing_slashes() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_rename() { - run("path_rename", true).await.unwrap() + run("path_rename", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_symlink_trailing_slashes() { - run("path_symlink_trailing_slashes", true).await.unwrap() + run("path_symlink_trailing_slashes", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[should_panic] @@ -270,7 +272,7 @@ async fn poll_oneoff_stdio() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn readlink() { - run("readlink", true).await.unwrap() + run("readlink", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[should_panic] @@ -281,37 +283,37 @@ async fn remove_directory_trailing_slashes() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn remove_nonempty_directory() { - run("remove_nonempty_directory", true).await.unwrap() + run("remove_nonempty_directory", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn renumber() { - run("renumber", true).await.unwrap() + run("renumber", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn sched_yield() { - run("sched_yield", true).await.unwrap() + run("sched_yield", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn stdio() { - run("stdio", true).await.unwrap() + run("stdio", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_create() { - run("symlink_create", true).await.unwrap() + run("symlink_create", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_filestat() { - run("symlink_filestat", true).await.unwrap() + run("symlink_filestat", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_loop() { - run("symlink_loop", true).await.unwrap() + run("symlink_loop", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn unlink_file_trailing_slashes() { - run("unlink_file_trailing_slashes", true).await.unwrap() + run("unlink_file_trailing_slashes", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_preopen() { - run("path_open_preopen", true).await.unwrap() + run("path_open_preopen", false).await.unwrap() } From dd159983a9d5244c33dd38dab99c7aa807215a0a Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 15:41:44 -0700 Subject: [PATCH 117/118] wasi-preview2-components: apparently inherit_stdio was on everywhere here as well. turn it off except for poll_oneoff_stdio --- .../tests/wasi-preview2-components.rs | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/crates/test-programs/tests/wasi-preview2-components.rs b/crates/test-programs/tests/wasi-preview2-components.rs index c0a77443c976..021438d55814 100644 --- a/crates/test-programs/tests/wasi-preview2-components.rs +++ b/crates/test-programs/tests/wasi-preview2-components.rs @@ -110,124 +110,126 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { // tests which fail. #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn big_random_buf() { - run("big_random_buf", true).await.unwrap() + run("big_random_buf", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn clock_time_get() { - run("clock_time_get", true).await.unwrap() + run("clock_time_get", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn close_preopen() { - run("close_preopen", true).await.unwrap() + run("close_preopen", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dangling_fd() { - run("dangling_fd", true).await.unwrap() + run("dangling_fd", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dangling_symlink() { - run("dangling_symlink", true).await.unwrap() + run("dangling_symlink", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn directory_seek() { - run("directory_seek", true).await.unwrap() + run("directory_seek", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn dir_fd_op_failures() { - run("dir_fd_op_failures", true).await.unwrap() + run("dir_fd_op_failures", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_advise() { - run("fd_advise", true).await.unwrap() + run("fd_advise", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_filestat_get() { - run("fd_filestat_get", true).await.unwrap() + run("fd_filestat_get", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_filestat_set() { - run("fd_filestat_set", true).await.unwrap() + run("fd_filestat_set", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_flags_set() { - run("fd_flags_set", true).await.unwrap() + run("fd_flags_set", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn fd_readdir() { - run("fd_readdir", true).await.unwrap() + run("fd_readdir", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_allocate() { - run("file_allocate", true).await.unwrap() + run("file_allocate", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_pread_pwrite() { - run("file_pread_pwrite", true).await.unwrap() + run("file_pread_pwrite", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_seek_tell() { - run("file_seek_tell", true).await.unwrap() + run("file_seek_tell", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_truncation() { - run("file_truncation", true).await.unwrap() + run("file_truncation", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn file_unbuffered_write() { - run("file_unbuffered_write", true).await.unwrap() + run("file_unbuffered_write", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[cfg_attr(windows, should_panic)] async fn interesting_paths() { - run("interesting_paths", true).await.unwrap() + run("interesting_paths", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn isatty() { - run("isatty", true).await.unwrap() + run("isatty", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn nofollow_errors() { - run("nofollow_errors", true).await.unwrap() + run("nofollow_errors", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn overwrite_preopen() { - run("overwrite_preopen", true).await.unwrap() + run("overwrite_preopen", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_exists() { - run("path_exists", true).await.unwrap() + run("path_exists", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_filestat() { - run("path_filestat", true).await.unwrap() + run("path_filestat", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_link() { - run("path_link", true).await.unwrap() + run("path_link", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_create_existing() { - run("path_open_create_existing", true).await.unwrap() + run("path_open_create_existing", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_read_write() { - run("path_open_read_write", true).await.unwrap() + run("path_open_read_write", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_dirfd_not_dir() { - run("path_open_dirfd_not_dir", true).await.unwrap() + run("path_open_dirfd_not_dir", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_missing() { - run("path_open_missing", true).await.unwrap() + run("path_open_missing", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_nonblock() { - run("path_open_nonblock", true).await.unwrap() + run("path_open_nonblock", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_rename_dir_trailing_slashes() { - run("path_rename_dir_trailing_slashes", true).await.unwrap() + run("path_rename_dir_trailing_slashes", false) + .await + .unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[should_panic] @@ -238,11 +240,11 @@ async fn path_rename_file_trailing_slashes() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_rename() { - run("path_rename", true).await.unwrap() + run("path_rename", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_symlink_trailing_slashes() { - run("path_symlink_trailing_slashes", true).await.unwrap() + run("path_symlink_trailing_slashes", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn poll_oneoff_files() { @@ -256,7 +258,7 @@ async fn poll_oneoff_stdio() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn readlink() { - run("readlink", true).await.unwrap() + run("readlink", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] #[should_panic] @@ -267,37 +269,37 @@ async fn remove_directory_trailing_slashes() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn remove_nonempty_directory() { - run("remove_nonempty_directory", true).await.unwrap() + run("remove_nonempty_directory", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn renumber() { - run("renumber", true).await.unwrap() + run("renumber", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn sched_yield() { - run("sched_yield", true).await.unwrap() + run("sched_yield", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn stdio() { - run("stdio", true).await.unwrap() + run("stdio", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_create() { - run("symlink_create", true).await.unwrap() + run("symlink_create", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_filestat() { - run("symlink_filestat", true).await.unwrap() + run("symlink_filestat", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn symlink_loop() { - run("symlink_loop", true).await.unwrap() + run("symlink_loop", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn unlink_file_trailing_slashes() { - run("unlink_file_trailing_slashes", true).await.unwrap() + run("unlink_file_trailing_slashes", false).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn path_open_preopen() { - run("path_open_preopen", true).await.unwrap() + run("path_open_preopen", false).await.unwrap() } From 6aae4ea543fb83704140e4299704ce61815c11a4 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 21 Jul 2023 17:10:36 -0700 Subject: [PATCH 118/118] extend timeout for riscv64 i suppose --- crates/wasi/src/preview2/pipe.rs | 45 +++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/wasi/src/preview2/pipe.rs b/crates/wasi/src/preview2/pipe.rs index e720ef93dbc5..02e2ca0962e2 100644 --- a/crates/wasi/src/preview2/pipe.rs +++ b/crates/wasi/src/preview2/pipe.rs @@ -400,6 +400,9 @@ mod test { use super::*; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + // 10ms was enough for every CI platform except linux riscv64: + const REASONABLE_DURATION: std::time::Duration = std::time::Duration::from_millis(100); + pub fn simplex(size: usize) -> (impl AsyncRead, impl AsyncWrite) { let (a, b) = tokio::io::duplex(size); let (_read_half, write_half) = tokio::io::split(a); @@ -421,7 +424,7 @@ mod test { // The reader task hasn't run yet. Call `ready` to await and fill the buffer. StreamState::Open => { - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -440,7 +443,7 @@ mod test { assert_eq!(state, StreamState::Open); if bs.is_empty() { // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -477,7 +480,7 @@ mod test { assert_eq!(state, StreamState::Open); if bs.is_empty() { // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -497,7 +500,7 @@ mod test { StreamState::Closed => {} // Correct! StreamState::Open => { // Need to await to give this side time to catch up - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -522,7 +525,7 @@ mod test { assert_eq!(state, StreamState::Open); if bs.is_empty() { // Reader task hasn't run yet. Call `ready` to await and fill the buffer. - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -540,7 +543,7 @@ mod test { assert_eq!(state, StreamState::Open); // We can wait on readiness and it will time out: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .err() .expect("the reader should time out"); @@ -555,7 +558,7 @@ mod test { // Wait readiness (yes we could possibly win the race and read it out faster, leaving that // out of the test for simplicity) - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("the ready is ok"); @@ -571,7 +574,7 @@ mod test { assert_eq!(state, StreamState::Open); // We can wait on readiness and it will time out: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .err() .expect("the reader should time out"); @@ -586,7 +589,7 @@ mod test { // Wait readiness (yes we could possibly win the race and read it out faster, leaving that // out of the test for simplicity) - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("the ready is ok"); @@ -611,7 +614,7 @@ mod test { w }); - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -623,7 +626,7 @@ mod test { assert_eq!(state, StreamState::Open); // Allow the crank to turn more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -635,14 +638,14 @@ mod test { assert_eq!(state, StreamState::Open); // The writer task is now finished - join with it: - let w = tokio::time::timeout(std::time::Duration::from_millis(10), writer_task) + let w = tokio::time::timeout(REASONABLE_DURATION, writer_task) .await .expect("the join should be ready instantly"); // And close the pipe: drop(w); // Allow the crank to turn more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.ready()) + tokio::time::timeout(REASONABLE_DURATION, reader.ready()) .await .expect("the reader should be ready instantly") .expect("ready is ok"); @@ -668,7 +671,7 @@ mod test { assert_eq!(len, 0); assert_eq!(state, StreamState::Open); - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok"); @@ -694,7 +697,7 @@ mod test { assert_eq!(state, StreamState::Open); // Check write readiness: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok"); @@ -729,7 +732,7 @@ mod test { assert_eq!(state, StreamState::Open); // After the write, still ready for more writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok"); @@ -746,7 +749,7 @@ mod test { assert_eq!(state, StreamState::Open); // After the write, still ready for more writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok"); @@ -772,7 +775,7 @@ mod test { assert_eq!(state, StreamState::Open); // turn the crank and it should be ready for writing again: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok"); @@ -788,7 +791,7 @@ mod test { assert_eq!(state, StreamState::Open); // turn the crank and it should Not become ready for writing until we read something out. - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .err() .expect("the writer should be not become ready"); @@ -803,7 +806,7 @@ mod test { reader.read_exact(&mut buf).await.unwrap(); // and no more: - tokio::time::timeout(std::time::Duration::from_millis(10), reader.read(&mut buf)) + tokio::time::timeout(REASONABLE_DURATION, reader.read(&mut buf)) .await .err() .expect("nothing more buffered in the system"); @@ -811,7 +814,7 @@ mod test { // Now the backpressure should be cleared, and an additional write should be accepted. // immediately ready for writing: - tokio::time::timeout(std::time::Duration::from_millis(10), writer.ready()) + tokio::time::timeout(REASONABLE_DURATION, writer.ready()) .await .expect("the writer should be ready instantly") .expect("ready is ok");