diff --git a/lib/wasi/src/runners/container.rs b/lib/wasi/src/runners/container.rs index 810f5b6c0ca..3a9a45c4acf 100644 --- a/lib/wasi/src/runners/container.rs +++ b/lib/wasi/src/runners/container.rs @@ -111,17 +111,15 @@ impl WapmContainer { /// Get the entire container as a single filesystem and a list of suggested /// directories to preopen. - pub(crate) fn container_fs(&self) -> (Box, Vec) { + pub(crate) fn container_fs(&self) -> Arc { match &self.repr { Repr::V1Mmap(mapped) => { let fs = WebcFileSystem::init_all(Arc::clone(mapped)); - let top_level_dirs = fs.top_level_dirs().clone(); - (Box::new(fs), top_level_dirs) + Arc::new(fs) } Repr::V1Owned(owned) => { let fs = WebcFileSystem::init_all(Arc::clone(owned)); - let top_level_dirs = fs.top_level_dirs().clone(); - (Box::new(fs), top_level_dirs) + Arc::new(fs) } } } diff --git a/lib/wasi/src/runners/mod.rs b/lib/wasi/src/runners/mod.rs index 95206dd097e..d08667ba8df 100644 --- a/lib/wasi/src/runners/mod.rs +++ b/lib/wasi/src/runners/mod.rs @@ -5,6 +5,8 @@ mod runner; pub mod emscripten; #[cfg(feature = "webc_runner_rt_wasi")] pub mod wasi; +#[cfg(any(feature = "webc_runner_rt_wasi", feature = "webc_runner_rt_wcgi"))] +mod wasi_common; #[cfg(feature = "webc_runner_rt_wcgi")] pub mod wcgi; @@ -13,10 +15,8 @@ pub use self::{ runner::Runner, }; -use std::path::PathBuf; - #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct MappedDirectory { - pub host: PathBuf, + pub host: std::path::PathBuf, pub guest: String, } diff --git a/lib/wasi/src/runners/wasi.rs b/lib/wasi/src/runners/wasi.rs index 47e68ffbda5..7476fb1252d 100644 --- a/lib/wasi/src/runners/wasi.rs +++ b/lib/wasi/src/runners/wasi.rs @@ -1,50 +1,39 @@ //! WebC container support for running WASI modules -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::sync::Arc; -use crate::{ - runners::{MappedDirectory, WapmContainer}, - PluggableRuntime, VirtualTaskManager, -}; -use crate::{WasiEnv, WasiEnvBuilder}; use anyhow::{Context, Error}; use serde::{Deserialize, Serialize}; -use virtual_fs::{FileSystem, OverlayFileSystem, RootFileSystemBuilder, TraceFileSystem}; use wasmer::{Module, Store}; use webc::metadata::{annotations::Wasi, Command}; +use crate::{ + runners::{wasi_common::CommonWasiOptions, MappedDirectory, WapmContainer}, + PluggableRuntime, VirtualTaskManager, WasiEnvBuilder, +}; + #[derive(Debug, Serialize, Deserialize)] pub struct WasiRunner { - args: Vec, - env: HashMap, - forward_host_env: bool, - mapped_dirs: Vec, + wasi: CommonWasiOptions, #[serde(skip, default)] store: Store, #[serde(skip, default)] - tasks: Option>, + pub(crate) tasks: Option>, } impl WasiRunner { /// Constructs a new `WasiRunner` given an `Store` pub fn new(store: Store) -> Self { Self { - args: Vec::new(), - env: HashMap::new(), store, - mapped_dirs: Vec::new(), - forward_host_env: false, + wasi: CommonWasiOptions::default(), tasks: None, } } /// Returns the current arguments for this `WasiRunner` pub fn get_args(&self) -> Vec { - self.args.clone() + self.wasi.args.clone() } /// Builder method to provide CLI args to the runner @@ -63,7 +52,7 @@ impl WasiRunner { A: IntoIterator, S: Into, { - self.args = args.into_iter().map(|s| s.into()).collect(); + self.wasi.args = args.into_iter().map(|s| s.into()).collect(); } /// Builder method to provide environment variables to the runner. @@ -74,7 +63,7 @@ impl WasiRunner { /// Provide environment variables to the runner. pub fn set_env(&mut self, key: impl Into, value: impl Into) { - self.env.insert(key.into(), value.into()); + self.wasi.env.insert(key.into(), value.into()); } pub fn with_envs(mut self, envs: I) -> Self @@ -94,7 +83,7 @@ impl WasiRunner { V: Into, { for (key, value) in envs { - self.env.insert(key.into(), value.into()); + self.wasi.env.insert(key.into(), value.into()); } } @@ -104,7 +93,7 @@ impl WasiRunner { } pub fn set_forward_host_env(&mut self) { - self.forward_host_env = true; + self.wasi.forward_host_env = true; } pub fn with_mapped_directories(mut self, dirs: I) -> Self @@ -112,7 +101,9 @@ impl WasiRunner { I: IntoIterator, D: Into, { - self.mapped_dirs.extend(dirs.into_iter().map(|d| d.into())); + self.wasi + .mapped_dirs + .extend(dirs.into_iter().map(|d| d.into())); self } @@ -128,30 +119,12 @@ impl WasiRunner { fn prepare_webc_env( &self, container: &WapmContainer, - command: &str, + program_name: &str, + wasi: &Wasi, ) -> Result { - let mut builder = WasiEnv::builder(command).args(&self.args); - - let fs = prepare_filesystem(&self.mapped_dirs, container)?; - builder.set_fs(fs); - // builder.set_fs(Box::new(TraceFileSystem(OverlayFileSystem::new( - // container_fs, - // [root_fs], - // )))); - // builder.set_fs(Box::new(TraceFileSystem(container_fs))); - - builder.add_preopen_dir("/")?; - builder.add_preopen_dir(".")?; - - if self.forward_host_env { - for (k, v) in std::env::vars() { - builder.add_env(k, v); - } - } - - for (k, v) in &self.env { - builder.add_env(k, v); - } + let mut builder = + self.wasi + .prepare_webc_env(container.container_fs(), program_name, wasi)?; if let Some(tasks) = &self.tasks { let rt = PluggableRuntime::new(Arc::clone(tasks)); @@ -177,10 +150,10 @@ impl crate::runners::Runner for WasiRunner { command: &Command, container: &WapmContainer, ) -> Result { - let atom_name = match command.get_annotation("wasi")? { - Some(Wasi { atom, .. }) => atom, - None => command_name.to_string(), - }; + let wasi = command + .get_annotation("wasi")? + .unwrap_or_else(|| Wasi::new(command_name)); + let atom_name = &wasi.atom; let atom = container .get_atom(&atom_name) .with_context(|| format!("Unable to get the \"{atom_name}\" atom"))?; @@ -188,77 +161,9 @@ impl crate::runners::Runner for WasiRunner { let mut module = Module::new(&self.store, atom)?; module.set_name(&atom_name); - self.prepare_webc_env(container, &atom_name)?.run(module)?; + self.prepare_webc_env(container, &atom_name, &wasi)? + .run(module)?; Ok(()) } } - -fn prepare_filesystem( - mapped_dirs: &[MappedDirectory], - container: &WapmContainer, -) -> Result, Error> { - let root_fs = RootFileSystemBuilder::default().build(); - - if !mapped_dirs.is_empty() { - let host_fs: Arc = Arc::new(crate::default_fs_backing()); - - for mapped in mapped_dirs { - let MappedDirectory { host, guest } = mapped; - let guest = PathBuf::from(guest); - tracing::debug!( - guest=%guest.display(), - host=%host.display(), - "Mounting host folder", - ); - root_fs - .mount(guest.clone(), &host_fs, host.clone()) - .with_context(|| { - format!( - "Unable to mount \"{}\" to \"{}\"", - host.display(), - guest.display() - ) - })?; - } - } - - let (container_fs, preopen_dirs) = container.container_fs(); - - Ok(Box::new(OverlayFileSystem::new(root_fs, [container_fs]))) -} - -#[cfg(test)] -mod tests { - use tempfile::TempDir; - - use super::*; - - const PYTHON: &[u8] = include_bytes!("../../../c-api/examples/assets/python-0.1.0.wasmer"); - - #[test] - fn python_use_case() { - let temp = TempDir::new().unwrap(); - let sub_dir = temp.path().join("path").join("to"); - std::fs::create_dir_all(&sub_dir).unwrap(); - std::fs::write(sub_dir.join("file.txt"), b"Hello, World!").unwrap(); - let mapping = [MappedDirectory { - guest: "/home".to_string(), - host: sub_dir, - }]; - let container = WapmContainer::from_bytes(PYTHON.into()).unwrap(); - - let fs = prepare_filesystem(&mapping, &container).unwrap(); - - assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file()); - assert!(fs.metadata("lib".as_ref()).unwrap().is_dir()); - assert!(fs - .metadata("lib/python3.6/collections/__init__.py".as_ref()) - .unwrap() - .is_file()); - assert!(fs - .metadata("lib/python3.6/encodings/__init__.py".as_ref()) - .unwrap() - .is_file()); - } -} diff --git a/lib/wasi/src/runners/wasi_common.rs b/lib/wasi/src/runners/wasi_common.rs new file mode 100644 index 00000000000..55ef2b74ff3 --- /dev/null +++ b/lib/wasi/src/runners/wasi_common.rs @@ -0,0 +1,167 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{Context, Error}; +use virtual_fs::{FileSystem, OverlayFileSystem, RootFileSystemBuilder}; +use webc::metadata::annotations::Wasi as WasiAnnotation; + +use crate::{runners::MappedDirectory, WasiEnv, WasiEnvBuilder}; + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct CommonWasiOptions { + pub(crate) args: Vec, + pub(crate) env: HashMap, + pub(crate) forward_host_env: bool, + pub(crate) mapped_dirs: Vec, +} + +impl CommonWasiOptions { + pub(crate) fn prepare_webc_env( + &self, + container_fs: Arc, + program_name: &str, + wasi: &WasiAnnotation, + ) -> Result { + let mut builder = WasiEnv::builder(program_name).args(&self.args); + + let fs = prepare_filesystem(&self.mapped_dirs, container_fs, |path| { + builder.add_preopen_dir(path).map_err(Error::from) + })?; + builder.set_fs(fs); + builder.add_preopen_dir("/")?; + builder.add_preopen_dir(".")?; + + self.populate_env(wasi, &mut builder); + self.populate_args(wasi, &mut builder); + + Ok(builder) + } + + fn populate_env(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) { + for item in wasi.env.as_deref().unwrap_or_default() { + // TODO(Michael-F-Bryan): Convert "wasi.env" in the webc crate from an + // Option> to a HashMap so we avoid this + // string.split() business + match item.split_once('=') { + Some((k, v)) => { + builder.add_env(k, v); + } + None => { + builder.add_env(item, String::new()); + } + } + } + + if self.forward_host_env { + builder.add_envs(std::env::vars()); + } + + builder.add_envs(self.env.clone()); + } + + fn populate_args(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) { + if let Some(main_args) = &wasi.main_args { + builder.add_args(main_args); + } + + builder.add_args(&self.args); + } +} + +fn prepare_filesystem( + mapped_dirs: &[MappedDirectory], + container_fs: Arc, + mut preopen: impl FnMut(&Path) -> Result<(), Error>, +) -> Result, Error> { + let root_fs = RootFileSystemBuilder::default().build(); + + if !mapped_dirs.is_empty() { + let host_fs: Arc = Arc::new(crate::default_fs_backing()); + dbg!(mapped_dirs); + + for dir in mapped_dirs { + let MappedDirectory { host, guest } = dir; + let guest = PathBuf::from(guest); + tracing::debug!( + guest=%guest.display(), + host=%host.display(), + "Mounting host folder", + ); + + if let Some(parent) = guest.parent() { + create_dir_all(&root_fs, parent).with_context(|| { + format!("Unable to create the \"{}\" directory", parent.display()) + })?; + } + + root_fs + .mount(guest.clone(), &host_fs, host.clone()) + .with_context(|| { + format!( + "Unable to mount \"{}\" to \"{}\"", + host.display(), + guest.display() + ) + })?; + + preopen(&guest) + .with_context(|| format!("Unable to preopen \"{}\"", guest.display()))?; + } + } + + Ok(Box::new(OverlayFileSystem::new(root_fs, [container_fs]))) +} + +fn create_dir_all(fs: &dyn FileSystem, path: &Path) -> Result<(), Error> { + if fs.metadata(path).is_ok() { + return Ok(()); + } + + if let Some(parent) = path.parent() { + create_dir_all(fs, parent)?; + } + + fs.create_dir(path)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use crate::runners::WapmContainer; + + use super::*; + + const PYTHON: &[u8] = include_bytes!("../../../c-api/examples/assets/python-0.1.0.wasmer"); + + #[test] + fn python_use_case() { + let temp = TempDir::new().unwrap(); + let sub_dir = temp.path().join("path").join("to"); + std::fs::create_dir_all(&sub_dir).unwrap(); + std::fs::write(sub_dir.join("file.txt"), b"Hello, World!").unwrap(); + let mapping = [MappedDirectory { + guest: "/home".to_string(), + host: sub_dir, + }]; + let container = WapmContainer::from_bytes(PYTHON.into()).unwrap(); + + let fs = prepare_filesystem(&mapping, container.container_fs(), |_| Ok(())).unwrap(); + + assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file()); + assert!(fs.metadata("lib".as_ref()).unwrap().is_dir()); + assert!(fs + .metadata("lib/python3.6/collections/__init__.py".as_ref()) + .unwrap() + .is_file()); + assert!(fs + .metadata("lib/python3.6/encodings/__init__.py".as_ref()) + .unwrap() + .is_file()); + } +} diff --git a/lib/wasi/src/runners/wcgi/handler.rs b/lib/wasi/src/runners/wcgi/handler.rs index 565f3be8b28..e04e23e0517 100644 --- a/lib/wasi/src/runners/wcgi/handler.rs +++ b/lib/wasi/src/runners/wcgi/handler.rs @@ -1,13 +1,6 @@ -use std::{ - collections::HashMap, - ops::Deref, - path::{Path, PathBuf}, - pin::Pin, - sync::Arc, - task::Poll, -}; +use std::{collections::HashMap, ops::Deref, pin::Pin, sync::Arc, task::Poll}; -use anyhow::{Context, Error}; +use anyhow::Error; use futures::{Future, FutureExt, StreamExt, TryFutureExt}; use http::{Request, Response}; use hyper::{service::Service, Body}; @@ -16,15 +9,12 @@ use tokio::{ runtime::Handle, }; use tracing::Instrument; -use virtual_fs::{FileSystem, PassthruFileSystem, RootFileSystemBuilder, TmpFileSystem}; use wasmer::Module; use wcgi_host::CgiDialect; use crate::{ - capabilities::Capabilities, - http::HttpClientCapabilityV1, - runners::{wcgi::Callbacks, MappedDirectory, WapmContainer}, - Pipe, PluggableRuntime, VirtualTaskManager, WasiEnv, + capabilities::Capabilities, http::HttpClientCapabilityV1, runners::wcgi::Callbacks, Pipe, + PluggableRuntime, VirtualTaskManager, WasiEnvBuilder, }; /// The shared object that manages the instantiaion of WASI executables and @@ -37,6 +27,7 @@ impl Handler { Handler(Arc::new(state)) } + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn handle(&self, req: Request) -> Result, Error> { tracing::debug!(headers=?req.headers()); @@ -48,17 +39,14 @@ impl Handler { tracing::debug!("Creating the WebAssembly instance"); - let builder = WasiEnv::builder(self.program.to_string()); - let mut request_specific_env = HashMap::new(); self.dialect .prepare_environment_variables(parts, &mut request_specific_env); let rt = PluggableRuntime::new(Arc::clone(&self.task_manager)); - let mut builder = builder - .envs(self.env.iter()) + + let builder = (self.setup_builder)()? .envs(request_specific_env) - .args(self.args.iter()) .stdin(Box::new(req_body_receiver)) .stdout(Box::new(res_body_sender)) .stderr(Box::new(stderr_sender)) @@ -67,15 +55,7 @@ impl Handler { http_client: HttpClientCapabilityV1::new_allow_all(), threading: Default::default(), }) - .runtime(Arc::new(rt)) - .fs(Box::new(self.fs()?)) - .preopen_dir(Path::new("/"))?; - - for mapping in &self.mapped_dirs { - builder - .add_preopen_dir(&mapping.guest) - .with_context(|| format!("Unable to preopen \"{}\"", mapping.guest))?; - } + .runtime(Arc::new(rt)); let module = self.module.clone(); @@ -135,45 +115,6 @@ impl Handler { Ok(response) } - - fn fs(&self) -> Result { - let root_fs = RootFileSystemBuilder::new().build(); - - if !self.mapped_dirs.is_empty() { - let fs_backing: Arc = - Arc::new(PassthruFileSystem::new(crate::default_fs_backing())); - - for MappedDirectory { host, guest } in self.mapped_dirs.iter() { - let guest = match guest.starts_with('/') { - true => PathBuf::from(guest), - false => Path::new("/").join(guest), - }; - tracing::debug!( - host=%host.display(), - guest=%guest.display(), - "mounting host directory", - ); - - if let Some(parent) = guest.parent() { - create_dir_all(&root_fs, parent) - .with_context(|| format!("Unable to create \"{}\"", parent.display()))?; - } - - root_fs - .mount(guest.clone(), &fs_backing, host.clone()) - .with_context(|| { - format!( - "Unable to mount \"{}\" to \"{}\"", - host.display(), - guest.display() - ) - }) - .map_err(|e| dbg!(e))?; - } - } - - Ok(root_fs) - } } impl Deref for Handler { @@ -184,20 +125,6 @@ impl Deref for Handler { } } -fn create_dir_all(fs: &dyn FileSystem, path: &Path) -> Result<(), Error> { - if fs.metadata(path).is_ok() { - return Ok(()); - } - - if let Some(parent) = path.parent() { - create_dir_all(fs, parent)?; - } - - fs.create_dir(path)?; - - Ok(()) -} - /// Drive the request to completion by streaming the request body to the /// instance and waiting for it to exit. async fn drive_request_to_completion( @@ -270,19 +197,17 @@ async fn consume_stderr( } } -#[derive(Clone, derivative::Derivative)] +#[derive(derivative::Derivative)] #[derivative(Debug)] pub(crate) struct SharedState { - pub(crate) program: String, - pub(crate) env: HashMap, - pub(crate) args: Vec, - pub(crate) mapped_dirs: Vec, - pub(crate) container: WapmContainer, pub(crate) module: Module, pub(crate) dialect: CgiDialect, - pub(crate) task_manager: Arc, + #[derivative(Debug = "ignore")] + pub(crate) setup_builder: Box Result + Send + Sync>, #[derivative(Debug = "ignore")] pub(crate) callbacks: Arc, + #[derivative(Debug = "ignore")] + pub(crate) task_manager: Arc, } impl Service> for Handler { diff --git a/lib/wasi/src/runners/wcgi/runner.rs b/lib/wasi/src/runners/wcgi/runner.rs index ed6dd3bfde8..de1f1c80238 100644 --- a/lib/wasi/src/runners/wcgi/runner.rs +++ b/lib/wasi/src/runners/wcgi/runner.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use std::{net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{Context, Error}; use futures::future::AbortHandle; @@ -7,6 +7,7 @@ use hyper::Body; use tower::{make::Shared, ServiceBuilder}; use tower_http::{catch_panic::CatchPanicLayer, cors::CorsLayer, trace::TraceLayer}; use tracing::Span; +use virtual_fs::FileSystem; use wasmer::{Engine, Module, Store}; use wcgi_host::CgiDialect; use webc::metadata::{ @@ -16,11 +17,12 @@ use webc::metadata::{ use crate::{ runners::{ + wasi_common::CommonWasiOptions, wcgi::handler::{Handler, SharedState}, MappedDirectory, WapmContainer, }, runtime::task_manager::tokio::TokioTaskManager, - VirtualTaskManager, + PluggableRuntime, VirtualTaskManager, WasiEnvBuilder, }; pub struct WcgiRunner { @@ -126,9 +128,6 @@ impl WcgiRunner { wasi: &Wasi, ctx: &RunnerContext<'_>, ) -> Result { - let env = construct_env(wasi, self.config.forward_host_env, &self.config.env); - let args = construct_args(wasi, &self.config.args); - let Wcgi { dialect, .. } = ctx.command().get_annotation("wcgi")?.unwrap_or_default(); let dialect = match dialect { @@ -137,68 +136,46 @@ impl WcgiRunner { }; let shared = SharedState { - program: self.program_name.clone(), - env, - args, - mapped_dirs: self.config.mapped_dirs.clone(), task_manager: self .config .task_manager .clone() .unwrap_or_else(|| Arc::new(TokioTaskManager::default())), module, - container: ctx.container.clone(), dialect, callbacks: Arc::clone(&self.config.callbacks), + setup_builder: Box::new(self.setup_builder(ctx, wasi)), }; Ok(Handler::new(shared)) } -} - -fn construct_args(wasi: &Wasi, extras: &[String]) -> Vec { - let mut args = Vec::new(); - - if let Some(main_args) = &wasi.main_args { - args.extend(main_args.iter().cloned()); - } - args.extend(extras.iter().cloned()); - - args -} - -fn construct_env( - wasi: &Wasi, - forward_host_env: bool, - overrides: &HashMap, -) -> HashMap { - let mut env: HashMap = HashMap::new(); - - for item in wasi.env.as_deref().unwrap_or_default() { - // TODO(Michael-F-Bryan): Convert "wasi.env" in the webc crate from an - // Option> to a HashMap so we avoid this - // string.split() business - match item.split_once('=') { - Some((k, v)) => { - env.insert(k.to_string(), v.to_string()); - } - None => { - env.insert(item.to_string(), String::new()); + fn setup_builder( + &self, + ctx: &RunnerContext<'_>, + wasi: &Wasi, + ) -> impl Fn() -> Result + Send + Sync { + let container_fs = ctx.container_fs(); + let wasi_common = self.config.wasi.clone(); + let program_name = self.program_name.clone(); + let wasi = wasi.clone(); + let tasks = self.config.task_manager.clone(); + + move || { + let mut builder = + wasi_common.prepare_webc_env(Arc::clone(&container_fs), &program_name, &wasi)?; + + if let Some(tasks) = &tasks { + let rt = PluggableRuntime::new(Arc::clone(tasks)); + builder.set_runtime(Arc::new(rt)); } - } - } - if forward_host_env { - env.extend(std::env::vars()); + Ok(builder) + } } - - env.extend(overrides.clone()); - - env } -// TODO(Michael-F-Bryan): Pass this to Runner::run() as "&dyn RunnerContext" +// TODO(Michael-F-Bryan): Pass this to Runner::run() as a "&dyn RunnerContext" // when we rewrite the "Runner" trait. struct RunnerContext<'a> { container: &'a WapmContainer, @@ -233,6 +210,10 @@ impl RunnerContext<'_> { // TODO(Michael-F-Bryan): wire this up to wasmer-cache Module::new(&self.engine, wasm).map_err(Error::from) } + + fn container_fs(&self) -> Arc { + self.container.container_fs() + } } impl crate::runners::Runner for WcgiRunner { @@ -265,11 +246,8 @@ impl crate::runners::Runner for WcgiRunner { #[derivative(Debug)] pub struct Config { task_manager: Option>, + wasi: CommonWasiOptions, addr: SocketAddr, - args: Vec, - env: HashMap, - forward_host_env: bool, - mapped_dirs: Vec, #[derivative(Debug = "ignore")] callbacks: Arc, store: Option>, @@ -288,7 +266,7 @@ impl Config { /// Add an argument to the WASI executable's command-line arguments. pub fn arg(&mut self, arg: impl Into) -> &mut Self { - self.args.push(arg.into()); + self.wasi.args.push(arg.into()); self } @@ -298,13 +276,13 @@ impl Config { A: IntoIterator, S: Into, { - self.args.extend(args.into_iter().map(|s| s.into())); + self.wasi.args.extend(args.into_iter().map(|s| s.into())); self } /// Expose an environment variable to the guest. pub fn env(&mut self, name: impl Into, value: impl Into) -> &mut Self { - self.env.insert(name.into(), value.into()); + self.wasi.env.insert(name.into(), value.into()); self } @@ -315,19 +293,20 @@ impl Config { K: Into, V: Into, { - self.env + self.wasi + .env .extend(variables.into_iter().map(|(k, v)| (k.into(), v.into()))); self } /// Forward all of the host's environment variables to the guest. pub fn forward_host_env(&mut self) -> &mut Self { - self.forward_host_env = true; + self.wasi.forward_host_env = true; self } pub fn map_directory(&mut self, dir: MappedDirectory) -> &mut Self { - self.mapped_dirs.push(dir); + self.wasi.mapped_dirs.push(dir); self } @@ -335,7 +314,7 @@ impl Config { &mut self, mappings: impl IntoIterator, ) -> &mut Self { - self.mapped_dirs.extend(mappings.into_iter()); + self.wasi.mapped_dirs.extend(mappings.into_iter()); self } @@ -357,10 +336,7 @@ impl Default for Config { Self { task_manager: None, addr: ([127, 0, 0, 1], 8000).into(), - env: HashMap::new(), - forward_host_env: false, - mapped_dirs: Vec::new(), - args: Vec::new(), + wasi: CommonWasiOptions::default(), callbacks: Arc::new(NoopCallbacks), store: None, } diff --git a/lib/wasi/src/state/builder.rs b/lib/wasi/src/state/builder.rs index 32d810197a5..21e871655e3 100644 --- a/lib/wasi/src/state/builder.rs +++ b/lib/wasi/src/state/builder.rs @@ -182,13 +182,27 @@ impl WasiEnvBuilder { Key: AsRef<[u8]>, Value: AsRef<[u8]>, { - env_pairs.into_iter().for_each(|(key, value)| { - self.add_env(key, value); - }); + self.add_envs(env_pairs); self } + /// Add multiple environment variable pairs. + /// + /// Both the key and value of the environment variables must not + /// contain a nul byte (`0x0`), and the key must not contain the + /// `=` byte (`0x3d`). + pub fn add_envs(&mut self, env_pairs: I) + where + I: IntoIterator, + Key: AsRef<[u8]>, + Value: AsRef<[u8]>, + { + for (key, value) in env_pairs { + self.add_env(key, value); + } + } + /// Get a reference to the configured environment variables. pub fn get_env(&self) -> &[(String, Vec)] { &self.envs @@ -231,13 +245,24 @@ impl WasiEnvBuilder { I: IntoIterator, Arg: AsRef<[u8]>, { - args.into_iter().for_each(|arg| { - self.add_arg(arg); - }); + self.add_args(args); self } + /// Add multiple arguments. + /// + /// Arguments must not contain the nul (0x0) byte + pub fn add_args(&mut self, args: I) + where + I: IntoIterator, + Arg: AsRef<[u8]>, + { + for arg in args { + self.add_arg(arg); + } + } + /// Get a reference to the configured arguments. pub fn get_args(&self) -> &[String] { &self.args diff --git a/tests/integration/cli/tests/run_unstable.rs b/tests/integration/cli/tests/run_unstable.rs index 0cdc44fb910..a1da778bc23 100644 --- a/tests/integration/cli/tests/run_unstable.rs +++ b/tests/integration/cli/tests/run_unstable.rs @@ -8,7 +8,7 @@ use std::{ time::{Duration, Instant}, }; -use assert_cmd::{assert::Assert, prelude::OutputAssertExt, Command}; +use assert_cmd::{assert::Assert, prelude::OutputAssertExt}; use predicates::str::contains; use reqwest::{blocking::Client, IntoUrl}; use tempfile::TempDir; @@ -16,6 +16,12 @@ use wasmer_integration_tests_cli::get_wasmer_path; const RUST_LOG: &str = "info,wasmer_wasi::runners=debug,virtual_fs::trace_fs=trace"; +fn wasmer_run_unstable() -> std::process::Command { + let mut cmd = std::process::Command::new(get_wasmer_path()); + cmd.env("RUST_LOG", RUST_LOG).arg("run-unstable"); + cmd +} + mod webc_on_disk { use super::*; use rand::Rng; @@ -26,13 +32,11 @@ mod webc_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn wasi_runner() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(fixtures::qjs()) .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -47,13 +51,11 @@ mod webc_on_disk { let temp = TempDir::new().unwrap(); std::fs::write(temp.path().join("index.js"), "console.log('Hello, World!')").unwrap(); - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(fixtures::qjs()) .arg(format!("--mapdir=/app:{}", temp.path().display())) .arg("--") .arg("/app/index.js") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -68,14 +70,12 @@ mod webc_on_disk { let temp = TempDir::new().unwrap(); std::fs::write(temp.path().join("main.py"), "print('Hello, World!')").unwrap(); - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(fixtures::python()) .arg(format!("--mapdir=/app:{}", temp.path().display())) .arg("--") .arg("-B") .arg("/app/main.py") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -87,11 +87,7 @@ mod webc_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn webc_files_with_multiple_commands_require_an_entrypoint_flag() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") - .arg(fixtures::wabt()) - .env("RUST_LOG", RUST_LOG) - .assert(); + let assert = wasmer_run_unstable().arg(fixtures::wabt()).assert(); let msg = r#"Unable to determine the WEBC file's entrypoint. Please choose one of ["wat2wasm", "wast2json", "wasm2wat", "wasm-interp", "wasm-validate", "wasm-strip"]"#; assert.failure().stderr(contains(msg)); @@ -103,15 +99,13 @@ mod webc_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn wasi_runner_with_env_vars() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(fixtures::python()) .arg("--env=SOME_VAR=Hello, World!") .arg("--") .arg("-B") .arg("-c") .arg("import os; print(os.environ['SOME_VAR'])") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -125,11 +119,9 @@ mod webc_on_disk { fn wcgi_runner() { // Start the WCGI server in the background let port = rand::thread_rng().gen_range(10_000_u16..u16::MAX); - let mut cmd = std::process::Command::new(get_wasmer_path()); - cmd.arg("run-unstable") - .arg(format!("--addr=127.0.0.1:{port}")) - .arg(fixtures::static_server()) - .env("RUST_LOG", RUST_LOG); + let mut cmd = wasmer_run_unstable(); + cmd.arg(format!("--addr=127.0.0.1:{port}")) + .arg(fixtures::static_server()); let child = JoinableChild::spawn(cmd); // make the request @@ -160,12 +152,10 @@ mod webc_on_disk { std::fs::write(temp.path().join("file.txt"), "Hello, World!").unwrap(); // Start the WCGI server in the background let port = rand::thread_rng().gen_range(10_000_u16..u16::MAX); - let mut cmd = std::process::Command::new(get_wasmer_path()); - cmd.arg("run-unstable") - .arg(format!("--addr=127.0.0.1:{port}")) + let mut cmd = wasmer_run_unstable(); + cmd.arg(format!("--addr=127.0.0.1:{port}")) .arg(format!("--mapdir=/path/to:{}", temp.path().display())) - .arg(fixtures::static_server()) - .env("RUST_LOG", RUST_LOG); + .arg(fixtures::static_server()); let child = JoinableChild::spawn(cmd); let body = http_get(format!("http://127.0.0.1:{port}/path/to/file.txt")).unwrap(); @@ -183,6 +173,8 @@ mod webc_on_disk { } mod wasm_on_disk { + use std::process::Command; + use super::*; use predicates::str::contains; @@ -192,13 +184,11 @@ mod wasm_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn wasi_executable() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(fixtures::qjs()) .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -210,11 +200,7 @@ mod wasm_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn no_abi() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") - .arg(fixtures::fib()) - .env("RUST_LOG", RUST_LOG) - .assert(); + let assert = wasmer_run_unstable().arg(fixtures::fib()).assert(); assert.success(); } @@ -225,11 +211,7 @@ mod wasm_on_disk { ignore = "wasmer run-unstable segfaults on musl" )] fn error_if_no_start_function_found() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") - .arg(fixtures::wat_no_start()) - .env("RUST_LOG", RUST_LOG) - .assert(); + let assert = wasmer_run_unstable().arg(fixtures::wat_no_start()).assert(); assert .failure() @@ -256,13 +238,11 @@ mod wasm_on_disk { assert!(dest.exists()); // Now we can try to run the compiled artifact - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(&dest) .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -279,13 +259,11 @@ fn wasmer_package_directory() { std::fs::copy(fixtures::qjs(), temp.path().join("qjs.wasm")).unwrap(); std::fs::copy(fixtures::qjs_wasmer_toml(), temp.path().join("wasmer.toml")).unwrap(); - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg(temp.path()) .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -300,15 +278,13 @@ mod remote_webc { ignore = "wasmer run-unstable segfaults on musl" )] fn quickjs_as_package_name() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg("saghul/quickjs") .arg("--entrypoint=quickjs") .arg("--registry=https://wapm.io/") .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!")); @@ -320,14 +296,12 @@ mod remote_webc { ignore = "wasmer run-unstable segfaults on musl" )] fn quickjs_as_url() { - let assert = Command::new(get_wasmer_path()) - .arg("run-unstable") + let assert = wasmer_run_unstable() .arg("https://wapm.io/saghul/quickjs") .arg("--entrypoint=quickjs") .arg("--") .arg("--eval") .arg("console.log('Hello, World!')") - .env("RUST_LOG", RUST_LOG) .assert(); assert.success().stdout(contains("Hello, World!"));