diff --git a/crates/test-programs/src/bin/p3_cli.rs b/crates/test-programs/src/bin/p3_cli.rs new file mode 100644 index 000000000000..b0a05c0ca933 --- /dev/null +++ b/crates/test-programs/src/bin/p3_cli.rs @@ -0,0 +1,40 @@ +use test_programs::p3::wasi::cli::{ + environment, stderr, stdin, stdout, terminal_stderr, terminal_stdin, terminal_stdout, +}; +use test_programs::p3::wit_stream; +use wit_bindgen::StreamResult; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + assert_eq!(environment::get_arguments(), ["p3_cli.component", "."]); + assert_ne!(environment::get_environment(), []); + assert_eq!(environment::initial_cwd(), None); + + assert!(terminal_stdin::get_terminal_stdin().is_none()); + assert!(terminal_stdout::get_terminal_stdout().is_none()); + assert!(terminal_stderr::get_terminal_stderr().is_none()); + + let mut stdin = stdin::get_stdin(); + assert!(stdin.next().await.is_none()); + + let (mut stdout_tx, stdout_rx) = wit_stream::new(); + stdout::set_stdout(stdout_rx); + let (res, buf) = stdout_tx.write(b"hello stdout\n".into()).await; + assert_eq!(res, StreamResult::Complete(13)); + assert_eq!(buf.into_vec(), []); + + let (mut stderr_tx, stderr_rx) = wit_stream::new(); + stderr::set_stderr(stderr_rx); + let (res, buf) = stderr_tx.write(b"hello stderr\n".into()).await; + assert_eq!(res, StreamResult::Complete(13)); + assert_eq!(buf.into_vec(), []); + + Ok(()) + } +} + +fn main() {} diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 0e7bd464644e..a225646dc138 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -61,6 +61,7 @@ preview1 = [ ] p3 = [ "wasmtime/component-model-async", + "wasmtime/component-model-async-bytes", ] [[test]] diff --git a/crates/wasi/src/cli.rs b/crates/wasi/src/cli.rs new file mode 100644 index 000000000000..3769a11d08a2 --- /dev/null +++ b/crates/wasi/src/cli.rs @@ -0,0 +1,95 @@ +use std::rc::Rc; +use std::sync::Arc; + +#[derive(Default)] +pub struct WasiCliCtx { + pub environment: Vec<(String, String)>, + pub arguments: Vec, + pub initial_cwd: Option, + pub stdin: I, + pub stdout: O, + pub stderr: O, +} + +pub trait IsTerminal { + /// Returns whether this stream is backed by a TTY. + fn is_terminal(&self) -> bool; +} + +impl IsTerminal for &T { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} + +impl IsTerminal for &mut T { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} + +impl IsTerminal for Box { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} + +impl IsTerminal for Rc { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} + +impl IsTerminal for Arc { + fn is_terminal(&self) -> bool { + T::is_terminal(self) + } +} + +impl IsTerminal for tokio::io::Empty { + fn is_terminal(&self) -> bool { + false + } +} + +impl IsTerminal for std::io::Empty { + fn is_terminal(&self) -> bool { + false + } +} + +impl IsTerminal for tokio::io::Stdin { + fn is_terminal(&self) -> bool { + std::io::stdin().is_terminal() + } +} + +impl IsTerminal for std::io::Stdin { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} + +impl IsTerminal for tokio::io::Stdout { + fn is_terminal(&self) -> bool { + std::io::stdout().is_terminal() + } +} + +impl IsTerminal for std::io::Stdout { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} + +impl IsTerminal for tokio::io::Stderr { + fn is_terminal(&self) -> bool { + std::io::stderr().is_terminal() + } +} + +impl IsTerminal for std::io::Stderr { + fn is_terminal(&self) -> bool { + std::io::IsTerminal::is_terminal(self) + } +} diff --git a/crates/wasi/src/clocks.rs b/crates/wasi/src/clocks.rs index 2618655424a9..bb030e1d1c32 100644 --- a/crates/wasi/src/clocks.rs +++ b/crates/wasi/src/clocks.rs @@ -2,29 +2,26 @@ use cap_std::time::{Duration, Instant, SystemClock}; use cap_std::{AmbientAuthority, ambient_authority}; use cap_time_ext::{MonotonicClockExt as _, SystemClockExt as _}; -#[repr(transparent)] -pub struct WasiClocksImpl(pub T); - impl WasiClocksView for &mut T { - fn clocks(&mut self) -> &WasiClocksCtx { - (**self).clocks() + fn clocks(&mut self) -> &mut WasiClocksCtx { + T::clocks(self) } } -impl WasiClocksView for WasiClocksImpl { - fn clocks(&mut self) -> &WasiClocksCtx { - self.0.clocks() +impl WasiClocksView for Box { + fn clocks(&mut self) -> &mut WasiClocksCtx { + T::clocks(self) } } impl WasiClocksView for WasiClocksCtx { - fn clocks(&mut self) -> &WasiClocksCtx { + fn clocks(&mut self) -> &mut WasiClocksCtx { self } } pub trait WasiClocksView: Send { - fn clocks(&mut self) -> &WasiClocksCtx; + fn clocks(&mut self) -> &mut WasiClocksCtx; } pub struct WasiClocksCtx { diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs index 4e057996bf02..e2cec900297e 100644 --- a/crates/wasi/src/ctx.rs +++ b/crates/wasi/src/ctx.rs @@ -1,3 +1,4 @@ +use crate::cli::WasiCliCtx; use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; use crate::net::{SocketAddrCheck, SocketAddrUse}; use crate::random::WasiRandomCtx; @@ -16,17 +17,17 @@ use std::sync::Arc; /// [p2::WasiCtxBuilder](crate::p2::WasiCtxBuilder) /// /// [`Store`]: wasmtime::Store -pub(crate) struct WasiCtxBuilder { - pub(crate) env: Vec<(String, String)>, - pub(crate) args: Vec, +#[derive(Default)] +pub(crate) struct WasiCtxBuilder { pub(crate) random: WasiRandomCtx, pub(crate) clocks: WasiClocksCtx, + pub(crate) cli: WasiCliCtx, pub(crate) socket_addr_check: SocketAddrCheck, pub(crate) allowed_network_uses: AllowedNetworkUses, pub(crate) allow_blocking_current_thread: bool, } -impl WasiCtxBuilder { +impl WasiCtxBuilder { /// Creates a builder for a new context with default parameters set. /// /// The current defaults are: @@ -44,20 +45,45 @@ impl WasiCtxBuilder { /// /// These defaults can all be updated via the various builder configuration /// methods below. - pub(crate) fn new() -> Self { + pub(crate) fn new(stdin: I, stdout: O, stderr: O) -> Self { let random = WasiRandomCtx::default(); let clocks = WasiClocksCtx::default(); + let cli = WasiCliCtx { + environment: Vec::default(), + arguments: Vec::default(), + initial_cwd: None, + stdin, + stdout, + stderr, + }; Self { - env: Vec::new(), - args: Vec::new(), random, clocks, + cli, socket_addr_check: SocketAddrCheck::default(), allowed_network_uses: AllowedNetworkUses::default(), allow_blocking_current_thread: false, } } + /// Provides a custom implementation of stdin to use. + pub fn stdin(&mut self, stdin: I) -> &mut Self { + self.cli.stdin = stdin; + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stdout. + pub fn stdout(&mut self, stdout: O) -> &mut Self { + self.cli.stdout = stdout; + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stderr. + pub fn stderr(&mut self, stderr: O) -> &mut Self { + self.cli.stderr = stderr; + self + } + /// Configures whether or not blocking operations made through this /// `WasiCtx` are allowed to block the current thread. /// @@ -97,7 +123,7 @@ impl WasiCtxBuilder { /// At this time environment variables are not deduplicated and if the same /// key is set twice then the guest will see two entries for the same key. pub fn envs(&mut self, env: &[(impl AsRef, impl AsRef)]) -> &mut Self { - self.env.extend( + self.cli.environment.extend( env.iter() .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())), ); @@ -109,7 +135,8 @@ impl WasiCtxBuilder { /// At this time environment variables are not deduplicated and if the same /// key is set twice then the guest will see two entries for the same key. pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { - self.env + self.cli + .environment .push((k.as_ref().to_owned(), v.as_ref().to_owned())); self } @@ -125,13 +152,15 @@ impl WasiCtxBuilder { /// Appends a list of arguments to the argument array to pass to wasm. pub fn args(&mut self, args: &[impl AsRef]) -> &mut Self { - self.args.extend(args.iter().map(|a| a.as_ref().to_owned())); + self.cli + .arguments + .extend(args.iter().map(|a| a.as_ref().to_owned())); self } /// Appends a single argument to get passed to wasm. pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.args.push(arg.as_ref().to_owned()); + self.cli.arguments.push(arg.as_ref().to_owned()); self } diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index 0fc8e6580d3d..57ac8e4ac09f 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -12,6 +12,7 @@ //! //! For WASIp3, see [`p3`]. WASIp3 support is experimental, unstable and incomplete. +pub mod cli; pub mod clocks; mod ctx; mod error; diff --git a/crates/wasi/src/p2/ctx.rs b/crates/wasi/src/p2/ctx.rs index 11a12f57705a..6d49d4c44f4d 100644 --- a/crates/wasi/src/p2/ctx.rs +++ b/crates/wasi/src/p2/ctx.rs @@ -1,3 +1,4 @@ +use crate::cli::WasiCliCtx; use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; use crate::ctx::AllowedNetworkUses; use crate::net::{SocketAddrCheck, SocketAddrUse}; @@ -36,10 +37,7 @@ use std::pin::Pin; /// /// [`Store`]: wasmtime::Store pub struct WasiCtxBuilder { - common: crate::WasiCtxBuilder, - stdin: Box, - stdout: Box, - stderr: Box, + common: crate::WasiCtxBuilder, Box>, preopens: Vec<(Dir, String)>, built: bool, } @@ -64,10 +62,11 @@ impl WasiCtxBuilder { /// methods below. pub fn new() -> Self { Self { - common: crate::WasiCtxBuilder::new(), - stdin: Box::new(pipe::ClosedInputStream), - stdout: Box::new(pipe::SinkOutputStream), - stderr: Box::new(pipe::SinkOutputStream), + common: crate::WasiCtxBuilder::new( + Box::new(pipe::ClosedInputStream), + Box::new(pipe::SinkOutputStream), + Box::new(pipe::SinkOutputStream), + ), preopens: Vec::new(), built: false, } @@ -88,19 +87,19 @@ impl WasiCtxBuilder { /// Note that inheriting the process's stdin can also be done through /// [`inherit_stdin`](WasiCtxBuilder::inherit_stdin). pub fn stdin(&mut self, stdin: impl StdinStream + 'static) -> &mut Self { - self.stdin = Box::new(stdin); + self.common.stdin(Box::new(stdin)); self } /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stdout. pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) -> &mut Self { - self.stdout = Box::new(stdout); + self.common.stdout(Box::new(stdout)); self } /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stderr. pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) -> &mut Self { - self.stderr = Box::new(stderr); + self.common.stderr(Box::new(stderr)); self } @@ -436,8 +435,6 @@ impl WasiCtxBuilder { let Self { common: crate::WasiCtxBuilder { - env, - args, random: WasiRandomCtx { random, @@ -449,13 +446,19 @@ impl WasiCtxBuilder { wall_clock, monotonic_clock, }, + cli: + WasiCliCtx { + environment: env, + arguments: args, + initial_cwd: _, + stdin, + stdout, + stderr, + }, socket_addr_check, allowed_network_uses, allow_blocking_current_thread, }, - stdin, - stdout, - stderr, preopens, built: _, } = mem::replace(self, Self::new()); diff --git a/crates/wasi/src/p2/stdio.rs b/crates/wasi/src/p2/stdio.rs index 8ff1799abbe2..eeb72c56b775 100644 --- a/crates/wasi/src/p2/stdio.rs +++ b/crates/wasi/src/p2/stdio.rs @@ -1,3 +1,4 @@ +use crate::cli::IsTerminal; use crate::p2::bindings::cli::{ stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, terminal_stdin, terminal_stdout, @@ -7,7 +8,6 @@ use crate::p2::{ InputStream, IoView, OutputStream, Pollable, StreamError, StreamResult, WasiImpl, WasiView, }; use bytes::Bytes; -use std::io::IsTerminal; use std::sync::Arc; use tokio::sync::Mutex; use wasmtime::component::Resource; @@ -20,7 +20,7 @@ use wasmtime_wasi_io::streams; /// /// Built-in implementations are provided for [`Stdin`], /// [`pipe::MemoryInputPipe`], and [`pipe::ClosedInputStream`]. -pub trait StdinStream: Send { +pub trait StdinStream: IsTerminal + Send { /// Creates a fresh stream which is reading stdin. /// /// Note that the returned stream must share state with all other streams @@ -33,17 +33,16 @@ pub trait StdinStream: Send { /// mean that all the others are no longer ready for reading. This is /// basically a consequence of the way the WIT APIs are designed today. fn stream(&self) -> Box; - - /// Returns whether this stream is backed by a TTY. - fn isatty(&self) -> bool; } impl StdinStream for pipe::MemoryInputPipe { fn stream(&self) -> Box { Box::new(self.clone()) } +} - fn isatty(&self) -> bool { +impl IsTerminal for pipe::MemoryInputPipe { + fn is_terminal(&self) -> bool { false } } @@ -52,8 +51,10 @@ impl StdinStream for pipe::ClosedInputStream { fn stream(&self) -> Box { Box::new(*self) } +} - fn isatty(&self) -> bool { +impl IsTerminal for pipe::ClosedInputStream { + fn is_terminal(&self) -> bool { false } } @@ -79,8 +80,10 @@ impl StdinStream for InputFile { file: Arc::clone(&self.file), }) } +} - fn isatty(&self) -> bool { +impl IsTerminal for InputFile { + fn is_terminal(&self) -> bool { false } } @@ -151,7 +154,10 @@ impl StdinStream for AsyncStdinStream { fn stream(&self) -> Box { Box::new(Self(self.0.clone())) } - fn isatty(&self) -> bool { +} + +impl IsTerminal for AsyncStdinStream { + fn is_terminal(&self) -> bool { false } } @@ -192,7 +198,7 @@ mod worker_thread_stdin; pub use self::worker_thread_stdin::{Stdin, stdin}; /// Similar to [`StdinStream`], except for output. -pub trait StdoutStream: Send { +pub trait StdoutStream: IsTerminal + Send { /// Returns a fresh new stream which can write to this output stream. /// /// Note that all output streams should output to the same logical source. @@ -206,17 +212,16 @@ pub trait StdoutStream: Send { /// /// Implementations must be able to handle this fn stream(&self) -> Box; - - /// Returns whether this stream is backed by a TTY. - fn isatty(&self) -> bool; } impl StdoutStream for pipe::MemoryOutputPipe { fn stream(&self) -> Box { Box::new(self.clone()) } +} - fn isatty(&self) -> bool { +impl IsTerminal for pipe::MemoryOutputPipe { + fn is_terminal(&self) -> bool { false } } @@ -225,8 +230,10 @@ impl StdoutStream for pipe::SinkOutputStream { fn stream(&self) -> Box { Box::new(*self) } +} - fn isatty(&self) -> bool { +impl IsTerminal for pipe::SinkOutputStream { + fn is_terminal(&self) -> bool { false } } @@ -235,8 +242,10 @@ impl StdoutStream for pipe::ClosedOutputStream { fn stream(&self) -> Box { Box::new(*self) } +} - fn isatty(&self) -> bool { +impl IsTerminal for pipe::ClosedOutputStream { + fn is_terminal(&self) -> bool { false } } @@ -262,8 +271,10 @@ impl StdoutStream for OutputFile { file: Arc::clone(&self.file), }) } +} - fn isatty(&self) -> bool { +impl IsTerminal for OutputFile { + fn is_terminal(&self) -> bool { false } } @@ -315,8 +326,10 @@ impl StdoutStream for Stdout { fn stream(&self) -> Box { Box::new(StdioOutputStream::Stdout) } +} - fn isatty(&self) -> bool { +impl IsTerminal for Stdout { + fn is_terminal(&self) -> bool { std::io::stdout().is_terminal() } } @@ -339,8 +352,10 @@ impl StdoutStream for Stderr { fn stream(&self) -> Box { Box::new(StdioOutputStream::Stderr) } +} - fn isatty(&self) -> bool { +impl IsTerminal for Stderr { + fn is_terminal(&self) -> bool { std::io::stderr().is_terminal() } } @@ -398,7 +413,10 @@ impl StdoutStream for AsyncStdoutStream { fn stream(&self) -> Box { Box::new(Self(self.0.clone())) } - fn isatty(&self) -> bool { +} + +impl IsTerminal for AsyncStdoutStream { + fn is_terminal(&self) -> bool { false } } @@ -520,7 +538,7 @@ where T: WasiView, { fn get_terminal_stdin(&mut self) -> anyhow::Result>> { - if self.ctx().stdin.isatty() { + if self.ctx().stdin.is_terminal() { let fd = self.table().push(TerminalInput)?; Ok(Some(fd)) } else { @@ -533,7 +551,7 @@ where T: WasiView, { fn get_terminal_stdout(&mut self) -> anyhow::Result>> { - if self.ctx().stdout.isatty() { + if self.ctx().stdout.is_terminal() { let fd = self.table().push(TerminalOutput)?; Ok(Some(fd)) } else { @@ -546,7 +564,7 @@ where T: WasiView, { fn get_terminal_stderr(&mut self) -> anyhow::Result>> { - if self.ctx().stderr.isatty() { + if self.ctx().stderr.is_terminal() { let fd = self.table().push(TerminalOutput)?; Ok(Some(fd)) } else { diff --git a/crates/wasi/src/p2/stdio/worker_thread_stdin.rs b/crates/wasi/src/p2/stdio/worker_thread_stdin.rs index bf7bef781b1e..7557e561d4d1 100644 --- a/crates/wasi/src/p2/stdio/worker_thread_stdin.rs +++ b/crates/wasi/src/p2/stdio/worker_thread_stdin.rs @@ -23,9 +23,10 @@ //! This module is one that's likely to change over time though as new systems //! are encountered along with preexisting bugs. +use crate::cli::IsTerminal; use crate::p2::stdio::StdinStream; use bytes::{Bytes, BytesMut}; -use std::io::{IsTerminal, Read}; +use std::io::Read; use std::mem; use std::sync::{Condvar, Mutex, OnceLock}; use tokio::sync::Notify; @@ -114,8 +115,10 @@ impl StdinStream for Stdin { fn stream(&self) -> Box { Box::new(Stdin) } +} - fn isatty(&self) -> bool { +impl IsTerminal for Stdin { + fn is_terminal(&self) -> bool { std::io::stdin().is_terminal() } } diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs index 4fb17e8ae15f..6a71e7be5b7b 100644 --- a/crates/wasi/src/p3/bindings.rs +++ b/crates/wasi/src/p3/bindings.rs @@ -12,9 +12,9 @@ //! done using the `with` option to [`bindgen!`]: //! //! ```rust -//! use wasmtime_wasi::p3::{WasiCtx, WasiView}; +//! use wasmtime_wasi::p3::{WasiCtx, WasiCtxView, WasiView}; //! use wasmtime::{Result, Engine, Config}; -//! use wasmtime::component::{Linker, HasSelf}; +//! use wasmtime::component::{Linker, HasSelf, ResourceTable}; //! //! wasmtime::component::bindgen!({ //! inline: " @@ -86,6 +86,7 @@ //! //! struct MyState { //! ctx: WasiCtx, +//! table: ResourceTable, //! } //! //! impl example::wasi::custom_host::Host for MyState { @@ -95,7 +96,12 @@ //! } //! //! impl WasiView for MyState { -//! fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView{ +//! ctx: &mut self.ctx, +//! table: &mut self.table, +//! } +//! } //! } //! //! fn main() -> Result<()> { @@ -165,6 +171,10 @@ mod generated { "wasi:sockets/types@0.3.0#[method]udp-socket.connect", ], }, + with: { + "wasi:cli/terminal-input/terminal-input": crate::p3::cli::TerminalInput, + "wasi:cli/terminal-output/terminal-output": crate::p3::cli::TerminalOutput, + } }); } pub use self::generated::LinkOptions; @@ -183,8 +193,8 @@ pub use self::generated::wasi::*; /// /// ```no_run /// use wasmtime::{Engine, Result, Store, Config}; -/// use wasmtime::component::{Component, Linker}; -/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime::component::{Component, Linker, ResourceTable}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiCtxView, WasiCtxBuilder, WasiView}; /// use wasmtime_wasi::p3::bindings::Command; /// /// // This example is an example shim of executing a component based on the @@ -214,6 +224,7 @@ pub use self::generated::wasi::*; /// &engine, /// MyState { /// ctx: builder.build(), +/// table: ResourceTable::default(), /// }, /// ); /// @@ -231,10 +242,16 @@ pub use self::generated::wasi::*; /// /// struct MyState { /// ctx: WasiCtx, +/// table: ResourceTable, /// } /// /// impl WasiView for MyState { -/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } /// } /// ``` /// @@ -250,8 +267,8 @@ pub use self::generated::Command; /// /// ```no_run /// use wasmtime::{Engine, Result, Store, Config}; -/// use wasmtime::component::{Linker, Component}; -/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime::component::{Linker, Component, ResourceTable}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; /// use wasmtime_wasi::p3::bindings::CommandPre; /// /// // This example is an example shim of executing a component based on the @@ -282,6 +299,7 @@ pub use self::generated::Command; /// &engine, /// MyState { /// ctx: builder.build(), +/// table: ResourceTable::default(), /// }, /// ); /// @@ -299,10 +317,16 @@ pub use self::generated::Command; /// /// struct MyState { /// ctx: WasiCtx, +/// table: ResourceTable, /// } /// /// impl WasiView for MyState { -/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } /// } /// ``` /// diff --git a/crates/wasi/src/p3/cli/host.rs b/crates/wasi/src/p3/cli/host.rs new file mode 100644 index 000000000000..5e0a4e345cb2 --- /dev/null +++ b/crates/wasi/src/p3/cli/host.rs @@ -0,0 +1,224 @@ +use crate::I32Exit; +use crate::cli::IsTerminal; +use crate::p3::bindings::cli::{ + environment, exit, stderr, stdin, stdout, terminal_input, terminal_output, terminal_stderr, + terminal_stdin, terminal_stdout, +}; +use crate::p3::cli::{TerminalInput, TerminalOutput, WasiCli, WasiCliCtxView}; +use anyhow::{Context as _, anyhow}; +use bytes::BytesMut; +use std::io::Cursor; +use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _}; +use wasmtime::component::{ + Accessor, AccessorTask, HasData, HostStream, Resource, StreamReader, StreamWriter, +}; + +struct InputTask { + rx: T, + tx: StreamWriter>, +} + +impl AccessorTask> for InputTask +where + U: HasData, + V: AsyncRead + Send + Sync + Unpin + 'static, +{ + async fn run(mut self, store: &Accessor) -> wasmtime::Result<()> { + let mut buf = BytesMut::with_capacity(8192); + while !self.tx.is_closed() { + match self.rx.read_buf(&mut buf).await { + Ok(0) => return Ok(()), + Ok(_) => { + buf = self + .tx + .write_all(store, Cursor::new(buf)) + .await + .into_inner(); + buf.clear(); + } + Err(_err) => { + // TODO: Report the error to the guest + return Ok(()); + } + } + } + Ok(()) + } +} + +struct OutputTask { + rx: StreamReader, + tx: T, +} + +impl AccessorTask> for OutputTask +where + U: HasData, + V: AsyncWrite + Send + Sync + Unpin + 'static, +{ + async fn run(mut self, store: &Accessor) -> wasmtime::Result<()> { + let mut buf = BytesMut::with_capacity(8192); + while !self.rx.is_closed() { + buf = self.rx.read(store, buf).await; + match self.tx.write_all(&buf).await { + Ok(()) => { + buf.clear(); + continue; + } + Err(_err) => { + // TODO: Report the error to the guest + return Ok(()); + } + } + } + Ok(()) + } +} + +impl terminal_input::Host for WasiCliCtxView<'_> {} +impl terminal_output::Host for WasiCliCtxView<'_> {} + +impl terminal_input::HostTerminalInput for WasiCliCtxView<'_> { + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.table + .delete(rep) + .context("failed to delete terminal input resource from table")?; + Ok(()) + } +} + +impl terminal_output::HostTerminalOutput for WasiCliCtxView<'_> { + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.table + .delete(rep) + .context("failed to delete terminal output resource from table")?; + Ok(()) + } +} + +impl terminal_stdin::Host for WasiCliCtxView<'_> { + fn get_terminal_stdin(&mut self) -> wasmtime::Result>> { + if self.ctx.stdin.is_terminal() { + let fd = self + .table + .push(TerminalInput) + .context("failed to push terminal stdin resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl terminal_stdout::Host for WasiCliCtxView<'_> { + fn get_terminal_stdout(&mut self) -> wasmtime::Result>> { + if self.ctx.stdout.is_terminal() { + let fd = self + .table + .push(TerminalOutput) + .context("failed to push terminal stdout resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl terminal_stderr::Host for WasiCliCtxView<'_> { + fn get_terminal_stderr(&mut self) -> wasmtime::Result>> { + if self.ctx.stderr.is_terminal() { + let fd = self + .table + .push(TerminalOutput) + .context("failed to push terminal stderr resource to table")?; + Ok(Some(fd)) + } else { + Ok(None) + } + } +} + +impl stdin::HostConcurrent for WasiCli { + async fn get_stdin(store: &Accessor) -> wasmtime::Result> { + store.with(|mut view| { + let instance = view.instance(); + let (tx, rx) = instance + .stream::<_, _, BytesMut>(&mut view) + .context("failed to create stream")?; + let stdin = view.get().ctx.stdin.reader(); + view.spawn(InputTask { + rx: Box::into_pin(stdin), + tx, + }); + Ok(rx.into()) + }) + } +} + +impl stdin::Host for WasiCliCtxView<'_> {} + +impl stdout::HostConcurrent for WasiCli { + async fn set_stdout( + store: &Accessor, + data: HostStream, + ) -> wasmtime::Result<()> { + store.with(|mut view| { + let stdout = data.into_reader(&mut view); + let tx = view.get().ctx.stdout.writer(); + view.spawn(OutputTask { + rx: stdout, + tx: Box::into_pin(tx), + }); + Ok(()) + }) + } +} + +impl stdout::Host for WasiCliCtxView<'_> {} + +impl stderr::HostConcurrent for WasiCli { + async fn set_stderr( + store: &Accessor, + data: HostStream, + ) -> wasmtime::Result<()> { + store.with(|mut view| { + let stderr = data.into_reader(&mut view); + let tx = view.get().ctx.stderr.writer(); + view.spawn(OutputTask { + rx: stderr, + tx: Box::into_pin(tx), + }); + Ok(()) + }) + } +} + +impl stderr::Host for WasiCliCtxView<'_> {} + +impl environment::Host for WasiCliCtxView<'_> { + fn get_environment(&mut self) -> wasmtime::Result> { + Ok(self.ctx.environment.clone()) + } + + fn get_arguments(&mut self) -> wasmtime::Result> { + Ok(self.ctx.arguments.clone()) + } + + fn initial_cwd(&mut self) -> wasmtime::Result> { + Ok(self.ctx.initial_cwd.clone()) + } +} + +impl exit::Host for WasiCliCtxView<'_> { + fn exit(&mut self, status: Result<(), ()>) -> wasmtime::Result<()> { + let status = match status { + Ok(()) => 0, + Err(()) => 1, + }; + Err(anyhow!(I32Exit(status))) + } + + fn exit_with_code(&mut self, status_code: u8) -> wasmtime::Result<()> { + Err(anyhow!(I32Exit(status_code.into()))) + } +} diff --git a/crates/wasi/src/p3/cli/mod.rs b/crates/wasi/src/p3/cli/mod.rs new file mode 100644 index 000000000000..ffa03d7edf4c --- /dev/null +++ b/crates/wasi/src/p3/cli/mod.rs @@ -0,0 +1,258 @@ +mod host; + +use crate::cli::{IsTerminal, WasiCliCtx}; +use crate::p3::bindings::cli; +use std::sync::Arc; +use tokio::io::{ + AsyncRead, AsyncWrite, Empty, Stderr, Stdin, Stdout, empty, stderr, stdin, stdout, +}; +use wasmtime::component::{HasData, Linker, ResourceTable}; + +pub struct WasiCliCtxView<'a> { + pub ctx: &'a mut WasiCliCtx, Box>, + pub table: &'a mut ResourceTable, +} + +impl WasiCliView for &mut T { + fn cli(&mut self) -> WasiCliCtxView<'_> { + T::cli(self) + } +} + +impl WasiCliView for Box { + fn cli(&mut self) -> WasiCliCtxView<'_> { + T::cli(self) + } +} + +pub trait WasiCliView: Send { + fn cli(&mut self) -> WasiCliCtxView<'_>; +} + +impl Default for WasiCliCtx, Box> { + fn default() -> Self { + Self { + environment: Vec::default(), + arguments: Vec::default(), + initial_cwd: None, + stdin: Box::new(empty()), + stdout: Box::new(empty()), + stderr: Box::new(empty()), + } + } +} + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this module. +/// +/// This is low-level API for advanced use cases, +/// [`wasmtime_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime_wasi::cli::WasiCliCtx; +/// use wasmtime_wasi::p3::cli::{InputStream, OutputStream, WasiCliView, WasiCliCtxView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::cli::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// cli: WasiCliCtx, Box>, +/// table: ResourceTable, +/// } +/// +/// impl WasiCliView for MyState { +/// fn cli(&mut self) -> WasiCliCtxView<'_> { +/// WasiCliCtxView { +/// ctx: &mut self.cli, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiCliView + 'static, +{ + let exit_options = cli::exit::LinkOptions::default(); + add_to_linker_impl(linker, &exit_options, T::cli) +} + +/// Similar to [`add_to_linker`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options( + linker: &mut Linker, + exit_options: &cli::exit::LinkOptions, +) -> anyhow::Result<()> +where + T: WasiCliView + 'static, +{ + add_to_linker_impl(linker, exit_options, T::cli) +} + +pub(crate) fn add_to_linker_impl( + linker: &mut Linker, + exit_options: &cli::exit::LinkOptions, + host_getter: fn(&mut T) -> WasiCliCtxView<'_>, +) -> wasmtime::Result<()> { + cli::exit::add_to_linker::<_, WasiCli>(linker, exit_options, host_getter)?; + cli::environment::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::stdin::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::stdout::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::stderr::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::terminal_input::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::terminal_output::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::terminal_stdin::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::terminal_stdout::add_to_linker::<_, WasiCli>(linker, host_getter)?; + cli::terminal_stderr::add_to_linker::<_, WasiCli>(linker, host_getter)?; + Ok(()) +} + +struct WasiCli; + +impl HasData for WasiCli { + type Data<'a> = WasiCliCtxView<'a>; +} + +pub struct TerminalInput; +pub struct TerminalOutput; + +pub trait InputStream: IsTerminal + Send { + fn reader(&self) -> Box; +} + +impl InputStream for &T { + fn reader(&self) -> Box { + T::reader(self) + } +} + +impl InputStream for &mut T { + fn reader(&self) -> Box { + T::reader(self) + } +} + +impl InputStream for Box { + fn reader(&self) -> Box { + T::reader(self) + } +} + +impl InputStream for Arc { + fn reader(&self) -> Box { + T::reader(self) + } +} + +impl InputStream for Empty { + fn reader(&self) -> Box { + Box::new(empty()) + } +} + +impl InputStream for std::io::Empty { + fn reader(&self) -> Box { + Box::new(empty()) + } +} + +impl InputStream for Stdin { + fn reader(&self) -> Box { + Box::new(stdin()) + } +} + +impl InputStream for std::io::Stdin { + fn reader(&self) -> Box { + Box::new(stdin()) + } +} + +pub trait OutputStream: IsTerminal + Send { + fn writer(&self) -> Box; +} + +impl OutputStream for &T { + fn writer(&self) -> Box { + T::writer(self) + } +} + +impl OutputStream for &mut T { + fn writer(&self) -> Box { + T::writer(self) + } +} + +impl OutputStream for Box { + fn writer(&self) -> Box { + T::writer(self) + } +} + +impl OutputStream for Arc { + fn writer(&self) -> Box { + T::writer(self) + } +} + +impl OutputStream for Empty { + fn writer(&self) -> Box { + Box::new(empty()) + } +} + +impl OutputStream for std::io::Empty { + fn writer(&self) -> Box { + Box::new(empty()) + } +} + +impl OutputStream for Stdout { + fn writer(&self) -> Box { + Box::new(stdout()) + } +} + +impl OutputStream for std::io::Stdout { + fn writer(&self) -> Box { + Box::new(stdout()) + } +} + +impl OutputStream for Stderr { + fn writer(&self) -> Box { + Box::new(stderr()) + } +} + +impl OutputStream for std::io::Stderr { + fn writer(&self) -> Box { + Box::new(stderr()) + } +} diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs index 5fb024a1d521..92fc75c28901 100644 --- a/crates/wasi/src/p3/clocks/host.rs +++ b/crates/wasi/src/p3/clocks/host.rs @@ -4,8 +4,9 @@ use cap_std::time::SystemTime; use tokio::time::sleep; use wasmtime::component::Accessor; +use crate::clocks::WasiClocksCtx; use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; -use crate::p3::clocks::{WasiClocks, WasiClocksImpl, WasiClocksView}; +use crate::p3::clocks::WasiClocks; impl TryFrom for wall_clock::Datetime { type Error = wasmtime::Error; @@ -21,12 +22,9 @@ impl TryFrom for wall_clock::Datetime { } } -impl wall_clock::Host for WasiClocksImpl -where - T: WasiClocksView, -{ +impl wall_clock::Host for WasiClocksCtx { fn now(&mut self) -> wasmtime::Result { - let now = self.clocks().wall_clock.now(); + let now = self.wall_clock.now(); Ok(wall_clock::Datetime { seconds: now.as_secs(), nanoseconds: now.subsec_nanos(), @@ -34,7 +32,7 @@ where } fn resolution(&mut self) -> wasmtime::Result { - let res = self.clocks().wall_clock.resolution(); + let res = self.wall_clock.resolution(); Ok(wall_clock::Datetime { seconds: res.as_secs(), nanoseconds: res.subsec_nanos(), @@ -42,15 +40,12 @@ where } } -impl monotonic_clock::HostConcurrent for WasiClocks -where - T: WasiClocksView + 'static, -{ +impl monotonic_clock::HostConcurrent for WasiClocks { async fn wait_until( store: &Accessor, when: monotonic_clock::Instant, ) -> wasmtime::Result<()> { - let clock_now = store.with(|mut view| view.get().clocks().monotonic_clock.now()); + let clock_now = store.with(|mut view| view.get().monotonic_clock.now()); if when > clock_now { sleep(Duration::from_nanos(when - clock_now)).await; }; @@ -68,15 +63,12 @@ where } } -impl monotonic_clock::Host for WasiClocksImpl -where - T: WasiClocksView, -{ +impl monotonic_clock::Host for WasiClocksCtx { fn now(&mut self) -> wasmtime::Result { - Ok(self.clocks().monotonic_clock.now()) + Ok(self.monotonic_clock.now()) } fn resolution(&mut self) -> wasmtime::Result { - Ok(self.clocks().monotonic_clock.resolution()) + Ok(self.monotonic_clock.resolution()) } } diff --git a/crates/wasi/src/p3/clocks/mod.rs b/crates/wasi/src/p3/clocks/mod.rs index 6d6167a5508b..ea9a4d2aff3f 100644 --- a/crates/wasi/src/p3/clocks/mod.rs +++ b/crates/wasi/src/p3/clocks/mod.rs @@ -1,6 +1,6 @@ mod host; -use crate::clocks::{WasiClocksImpl, WasiClocksView}; +use crate::clocks::{WasiClocksCtx, WasiClocksView}; use crate::p3::bindings::clocks; use wasmtime::component::{HasData, Linker}; @@ -48,28 +48,27 @@ use wasmtime::component::{HasData, Linker}; /// } /// /// impl WasiClocksView for MyState { -/// fn clocks(&mut self) -> &WasiClocksCtx { &self.clocks } +/// fn clocks(&mut self) -> &mut WasiClocksCtx { &mut self.clocks } /// } /// ``` -pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { - add_to_linker_impl(linker, |x| WasiClocksImpl(x)) +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiClocksView + 'static, +{ + add_to_linker_impl(linker, T::clocks) } -pub(crate) fn add_to_linker_impl( +pub(crate) fn add_to_linker_impl( linker: &mut Linker, - host_getter: fn(&mut T) -> WasiClocksImpl<&mut U>, -) -> wasmtime::Result<()> -where - T: Send, - U: WasiClocksView + 'static, -{ - clocks::monotonic_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; - clocks::wall_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; + host_getter: fn(&mut T) -> &mut WasiClocksCtx, +) -> wasmtime::Result<()> { + clocks::monotonic_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; + clocks::wall_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; Ok(()) } -struct WasiClocks(T); +struct WasiClocks; -impl HasData for WasiClocks { - type Data<'a> = WasiClocksImpl<&'a mut T>; +impl HasData for WasiClocks { + type Data<'a> = &'a mut WasiClocksCtx; } diff --git a/crates/wasi/src/p3/ctx.rs b/crates/wasi/src/p3/ctx.rs index 8e10e394fdfb..7a42fac4670b 100644 --- a/crates/wasi/src/p3/ctx.rs +++ b/crates/wasi/src/p3/ctx.rs @@ -1,5 +1,7 @@ +use crate::cli::WasiCliCtx; use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; use crate::net::SocketAddrUse; +use crate::p3::cli::{InputStream, OutputStream}; use crate::p3::filesystem::Dir; use crate::random::WasiRandomCtx; use crate::{DirPerms, FilePerms, OpenMode}; @@ -11,6 +13,7 @@ use std::mem; use std::net::SocketAddr; use std::path::Path; use std::pin::Pin; +use tokio::io::{empty, stderr, stdin, stdout}; /// Builder-style structure used to create a [`WasiCtx`]. /// @@ -33,15 +36,18 @@ use std::pin::Pin; /// /// [`Store`]: wasmtime::Store pub struct WasiCtxBuilder { - common: crate::WasiCtxBuilder, - // TODO: implement CLI and filesystem - stdin: Box<()>, - stdout: Box<()>, - stderr: Box<()>, + common: crate::WasiCtxBuilder, Box>, + // TODO: implement filesystem preopens: Vec<(Dir, String)>, built: bool, } +impl Default for crate::WasiCtxBuilder, Box> { + fn default() -> Self { + crate::WasiCtxBuilder::new(Box::new(empty()), Box::new(empty()), Box::new(empty())) + } +} + impl WasiCtxBuilder { /// Creates a builder for a new context with default parameters set. /// @@ -62,10 +68,7 @@ impl WasiCtxBuilder { /// methods below. pub fn new() -> Self { Self { - common: crate::WasiCtxBuilder::new(), - stdin: Box::default(), - stdout: Box::default(), - stderr: Box::default(), + common: crate::WasiCtxBuilder::default(), preopens: Vec::default(), built: false, } @@ -77,23 +80,20 @@ impl WasiCtxBuilder { /// /// Note that inheriting the process's stdin can also be done through /// [`inherit_stdin`](WasiCtxBuilder::inherit_stdin). - // TODO: implement - pub fn stdin(&mut self, stdin: ()) -> &mut Self { - self.stdin = Box::new(stdin); + pub fn stdin(&mut self, stdin: impl InputStream + 'static) -> &mut Self { + self.common.stdin(Box::new(stdin)); self } /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stdout. - // TODO: implement - pub fn stdout(&mut self, stdout: ()) -> &mut Self { - self.stdout = Box::new(stdout); + pub fn stdout(&mut self, stdout: impl OutputStream + 'static) -> &mut Self { + self.common.stdout(Box::new(stdout)); self } /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stderr. - // TODO: implement - pub fn stderr(&mut self, stderr: ()) -> &mut Self { - self.stderr = Box::new(stderr); + pub fn stderr(&mut self, stderr: impl OutputStream + 'static) -> &mut Self { + self.common.stderr(Box::new(stderr)); self } @@ -104,8 +104,7 @@ impl WasiCtxBuilder { /// when using this it's typically best to have a single wasm instance in /// the process using this. pub fn inherit_stdin(&mut self) -> &mut Self { - // TODO: implement - self.stdin(()) + self.stdin(stdin()) } /// Configures this context's stdout stream to write to the host process's @@ -114,8 +113,7 @@ impl WasiCtxBuilder { /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) /// multiple instances printing to stdout works well. pub fn inherit_stdout(&mut self) -> &mut Self { - // TODO: implement - self.stdout(()) + self.stdout(stdout()) } /// Configures this context's stderr stream to write to the host process's @@ -124,8 +122,7 @@ impl WasiCtxBuilder { /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) /// multiple instances printing to stderr works well. pub fn inherit_stderr(&mut self) -> &mut Self { - // TODO: implement - self.stderr(()) + self.stderr(stderr()) } /// Configures all of stdin, stdout, and stderr to be inherited from the @@ -430,13 +427,23 @@ impl WasiCtxBuilder { assert!(!self.built); let Self { - common: crate::WasiCtxBuilder { random, clocks, .. }, + common: + crate::WasiCtxBuilder { + random, + clocks, + cli, + .. + }, built: _, .. } = mem::replace(self, Self::new()); self.built = true; - WasiCtx { random, clocks } + WasiCtx { + random, + clocks, + cli, + } } } @@ -456,14 +463,21 @@ impl WasiCtxBuilder { /// # Example /// /// ``` -/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; /// /// struct MyState { /// ctx: WasiCtx, +/// table: ResourceTable, /// } /// /// impl WasiView for MyState { -/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } /// } /// /// impl MyState { @@ -475,6 +489,7 @@ impl WasiCtxBuilder { /// /// MyState { /// ctx: wasi.build(), +/// table: ResourceTable::default(), /// } /// } /// } @@ -483,6 +498,7 @@ impl WasiCtxBuilder { pub struct WasiCtx { pub random: WasiRandomCtx, pub clocks: WasiClocksCtx, + pub cli: WasiCliCtx, Box>, } impl WasiCtx { diff --git a/crates/wasi/src/p3/mod.rs b/crates/wasi/src/p3/mod.rs index 35d84d979277..36f2cc4e1224 100644 --- a/crates/wasi/src/p3/mod.rs +++ b/crates/wasi/src/p3/mod.rs @@ -9,6 +9,7 @@ //! Documentation of this module may be incorrect or out-of-sync with the implementation. pub mod bindings; +pub mod cli; pub mod clocks; mod ctx; pub mod filesystem; @@ -17,12 +18,11 @@ mod view; use wasmtime::component::Linker; -use crate::clocks::WasiClocksImpl; use crate::p3::bindings::LinkOptions; -use crate::random::WasiRandomImpl; +use crate::p3::cli::WasiCliCtxView; pub use self::ctx::{WasiCtx, WasiCtxBuilder}; -pub use self::view::{WasiImpl, WasiView}; +pub use self::view::{WasiCtxView, WasiView}; /// Add all WASI interfaces from this module into the `linker` provided. /// @@ -34,8 +34,8 @@ pub use self::view::{WasiImpl, WasiView}; /// /// ``` /// use wasmtime::{Engine, Result, Store, Config}; -/// use wasmtime::component::Linker; -/// use wasmtime_wasi::p3::{WasiCtx, WasiView}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiCtxView, WasiView}; /// /// fn main() -> Result<()> { /// let mut config = Config::new(); @@ -60,10 +60,16 @@ pub use self::view::{WasiImpl, WasiView}; /// #[derive(Default)] /// struct MyState { /// ctx: WasiCtx, +/// table: ResourceTable, /// } /// /// impl WasiView for MyState { -/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } /// } /// ``` pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> @@ -82,9 +88,14 @@ pub fn add_to_linker_with_options( where T: WasiView + 'static, { - // TODO: use options - _ = options; - clocks::add_to_linker_impl(linker, |x| WasiClocksImpl(&mut x.ctx().clocks))?; - random::add_to_linker_impl(linker, |x| WasiRandomImpl(&mut x.ctx().random))?; + clocks::add_to_linker_impl(linker, |x| &mut x.ctx().ctx.clocks)?; + random::add_to_linker_impl(linker, |x| &mut x.ctx().ctx.random)?; + cli::add_to_linker_impl(linker, &options.into(), |x| { + let WasiCtxView { ctx, table } = x.ctx(); + WasiCliCtxView { + ctx: &mut ctx.cli, + table, + } + })?; Ok(()) } diff --git a/crates/wasi/src/p3/random/host.rs b/crates/wasi/src/p3/random/host.rs index c23904ca4464..7e2b71640d71 100644 --- a/crates/wasi/src/p3/random/host.rs +++ b/crates/wasi/src/p3/random/host.rs @@ -2,46 +2,37 @@ use cap_rand::Rng; use cap_rand::distributions::Standard; use crate::p3::bindings::random::{insecure, insecure_seed, random}; -use crate::p3::random::{WasiRandomImpl, WasiRandomView}; +use crate::random::WasiRandomCtx; -impl random::Host for WasiRandomImpl -where - T: WasiRandomView, -{ +impl random::Host for WasiRandomCtx { fn get_random_bytes(&mut self, len: u64) -> wasmtime::Result> { - Ok((&mut self.random().random) + Ok((&mut self.random) .sample_iter(Standard) .take(len as usize) .collect()) } fn get_random_u64(&mut self) -> wasmtime::Result { - Ok(self.random().random.sample(Standard)) + Ok(self.random.sample(Standard)) } } -impl insecure::Host for WasiRandomImpl -where - T: WasiRandomView, -{ +impl insecure::Host for WasiRandomCtx { fn get_insecure_random_bytes(&mut self, len: u64) -> wasmtime::Result> { - Ok((&mut self.random().insecure_random) + Ok((&mut self.insecure_random) .sample_iter(Standard) .take(len as usize) .collect()) } fn get_insecure_random_u64(&mut self) -> wasmtime::Result { - Ok(self.random().insecure_random.sample(Standard)) + Ok(self.insecure_random.sample(Standard)) } } -impl insecure_seed::Host for WasiRandomImpl -where - T: WasiRandomView, -{ +impl insecure_seed::Host for WasiRandomCtx { fn insecure_seed(&mut self) -> wasmtime::Result<(u64, u64)> { - let seed: u128 = self.random().insecure_random_seed; + let seed: u128 = self.insecure_random_seed; Ok((seed as u64, (seed >> 64) as u64)) } } diff --git a/crates/wasi/src/p3/random/mod.rs b/crates/wasi/src/p3/random/mod.rs index 8f41008e2352..524bef75711c 100644 --- a/crates/wasi/src/p3/random/mod.rs +++ b/crates/wasi/src/p3/random/mod.rs @@ -1,7 +1,7 @@ mod host; use crate::p3::bindings::random; -use crate::random::{WasiRandomImpl, WasiRandomView}; +use crate::random::{WasiRandomCtx, WasiRandomView}; use wasmtime::component::{HasData, Linker}; /// Add all WASI interfaces from this module into the `linker` provided. @@ -19,7 +19,7 @@ use wasmtime::component::{HasData, Linker}; /// /// ``` /// use wasmtime::{Engine, Result, Store, Config}; -/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime::component::Linker; /// use wasmtime_wasi::random::{WasiRandomView, WasiRandomCtx}; /// /// fn main() -> Result<()> { @@ -51,26 +51,25 @@ use wasmtime::component::{HasData, Linker}; /// fn random(&mut self) -> &mut WasiRandomCtx { &mut self.random } /// } /// ``` -pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { - add_to_linker_impl(linker, |x| WasiRandomImpl(x)) +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiRandomView + 'static, +{ + add_to_linker_impl(linker, T::random) } -pub(crate) fn add_to_linker_impl( +pub(crate) fn add_to_linker_impl( linker: &mut Linker, - host_getter: fn(&mut T) -> WasiRandomImpl<&mut U>, -) -> wasmtime::Result<()> -where - T: Send, - U: WasiRandomView + 'static, -{ - random::random::add_to_linker::<_, WasiRandom>(linker, host_getter)?; - random::insecure::add_to_linker::<_, WasiRandom>(linker, host_getter)?; - random::insecure_seed::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + host_getter: fn(&mut T) -> &mut WasiRandomCtx, +) -> wasmtime::Result<()> { + random::random::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + random::insecure::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + random::insecure_seed::add_to_linker::<_, WasiRandom>(linker, host_getter)?; Ok(()) } -struct WasiRandom(T); +struct WasiRandom; -impl HasData for WasiRandom { - type Data<'a> = WasiRandomImpl<&'a mut T>; +impl HasData for WasiRandom { + type Data<'a> = &'a mut WasiRandomCtx; } diff --git a/crates/wasi/src/p3/view.rs b/crates/wasi/src/p3/view.rs index c455eae7dd77..7674f22051bd 100644 --- a/crates/wasi/src/p3/view.rs +++ b/crates/wasi/src/p3/view.rs @@ -1,5 +1,12 @@ +use wasmtime::component::ResourceTable; + use crate::p3::ctx::WasiCtx; +pub struct WasiCtxView<'a> { + pub ctx: &'a mut WasiCtx, + pub table: &'a mut ResourceTable, +} + /// A trait which provides access to the [`WasiCtx`] inside the embedder's `T` /// of [`Store`][`Store`]. /// @@ -12,14 +19,21 @@ use crate::p3::ctx::WasiCtx; /// # Example /// /// ``` -/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiView, WasiCtxView}; +/// use wasmtime::component::ResourceTable; /// /// struct MyState { /// ctx: WasiCtx, +/// table: ResourceTable, /// } /// /// impl WasiView for MyState { -/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView{ +/// ctx: &mut self.ctx, +/// table: &mut self.table, +/// } +/// } /// } /// ``` /// [`Store`]: wasmtime::Store @@ -28,37 +42,17 @@ use crate::p3::ctx::WasiCtx; pub trait WasiView: Send { /// Yields mutable access to the [`WasiCtx`] configuration used for this /// context. - fn ctx(&mut self) -> &mut WasiCtx; + fn ctx(&mut self) -> WasiCtxView<'_>; } impl WasiView for &mut T { - fn ctx(&mut self) -> &mut WasiCtx { + fn ctx(&mut self) -> WasiCtxView<'_> { T::ctx(self) } } impl WasiView for Box { - fn ctx(&mut self) -> &mut WasiCtx { + fn ctx(&mut self) -> WasiCtxView<'_> { T::ctx(self) } } - -/// A small newtype wrapper which serves as the basis for implementations of -/// `Host` WASI traits in this crate. -/// -/// This type is used as the basis for the implementation of all `Host` traits -/// generated by `bindgen!` for WASI interfaces. This is used automatically with -/// [`add_to_linker`](crate::p3::add_to_linker_sync). -/// -/// This type is otherwise provided if you're calling the `add_to_linker` -/// functions generated by `bindgen!` from the [`bindings` -/// module](crate::p3::bindings). In this situation you'll want to create a value of -/// this type in the closures added to a `Linker`. -#[repr(transparent)] -pub struct WasiImpl(pub T); - -impl WasiView for WasiImpl { - fn ctx(&mut self) -> &mut WasiCtx { - T::ctx(&mut self.0) - } -} diff --git a/crates/wasi/src/random.rs b/crates/wasi/src/random.rs index f3df9c6af695..474f819155cd 100644 --- a/crates/wasi/src/random.rs +++ b/crates/wasi/src/random.rs @@ -1,17 +1,14 @@ use cap_rand::{Rng as _, RngCore, SeedableRng as _}; -#[repr(transparent)] -pub struct WasiRandomImpl(pub T); - impl WasiRandomView for &mut T { fn random(&mut self) -> &mut WasiRandomCtx { - (**self).random() + T::random(self) } } -impl WasiRandomView for WasiRandomImpl { +impl WasiRandomView for Box { fn random(&mut self) -> &mut WasiRandomCtx { - self.0.random() + T::random(self) } } diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs index 891ee9f8d631..9f9d247b9c52 100644 --- a/crates/wasi/tests/all/p3/mod.rs +++ b/crates/wasi/tests/all/p3/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, anyhow}; use wasmtime::Store; use wasmtime::component::{Component, Linker, ResourceTable}; use wasmtime_wasi::p3::bindings::Command; -use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi::{DirPerms, FilePerms}; use test_programs_artifacts::*; @@ -23,8 +23,11 @@ struct Ctx { } impl WasiView for Ctx { - fn ctx(&mut self) -> &mut WasiCtx { - &mut self.p3 + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.p3, + table: &mut self.table, + } } } @@ -56,23 +59,28 @@ async fn run(path: &str) -> anyhow::Result<()> { .context("failed to link `wasi:cli@0.2.x`")?; wasmtime_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; - let mut builder = WasiCtxBuilder::new(); + let table = ResourceTable::default(); + + let p2 = wasmtime_wasi::p2::WasiCtx::builder() + .inherit_stdout() + .inherit_stderr() + .build(); + + let mut p3 = WasiCtxBuilder::new(); let name = path.file_stem().unwrap().to_str().unwrap(); let tempdir = tempfile::Builder::new() .prefix(&format!("wasi_components_{name}_",)) .tempdir()?; - builder - .args(&[name, "."]) + p3.args(&[name, "."]) .inherit_network() .allow_ip_name_lookup(true); println!("preopen: {tempdir:?}"); - builder.preopened_dir(tempdir.path(), ".", DirPerms::all(), FilePerms::all())?; + p3.preopened_dir(tempdir.path(), ".", DirPerms::all(), FilePerms::all())?; for (var, val) in test_programs_artifacts::wasi_tests_environment() { - builder.env(var, val); + p3.env(var, val); } - let table = ResourceTable::default(); - let p2 = wasmtime_wasi::p2::WasiCtx::builder().build(); - let p3 = builder.build(); + let p3 = p3.build(); + let mut store = Store::new(&engine, Ctx { table, p2, p3 }); let instance = linker.instantiate_async(&mut store, &component).await?; let command = @@ -98,3 +106,8 @@ async fn p3_clocks_sleep() -> anyhow::Result<()> { async fn p3_random_imports() -> anyhow::Result<()> { run(P3_RANDOM_IMPORTS_COMPONENT).await } + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_cli() -> anyhow::Result<()> { + run(P3_CLI_COMPONENT).await +} diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 097731db89c1..ddd3283952e1 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -840,10 +840,10 @@ impl wasmtime_wasi::p2::StdoutStream for LogStream { fn stream(&self) -> Box { Box::new(self.clone()) } +} - fn isatty(&self) -> bool { - use std::io::IsTerminal; - +impl wasmtime_wasi::cli::IsTerminal for LogStream { + fn is_terminal(&self) -> bool { match &self.output { Output::Stdout => std::io::stdout().is_terminal(), Output::Stderr => std::io::stderr().is_terminal(),