diff --git a/Cargo.lock b/Cargo.lock index 3b77b0e6fc6..bcfeccd4663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3370,9 +3370,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", @@ -5875,6 +5875,7 @@ dependencies = [ "predicates 2.1.5", "pretty_assertions", "rand", + "regex", "reqwest", "serde", "tar", diff --git a/lib/cli/src/cli.rs b/lib/cli/src/cli.rs index dfcaf117030..b18de7ba816 100644 --- a/lib/cli/src/cli.rs +++ b/lib/cli/src/cli.rs @@ -9,8 +9,7 @@ use crate::commands::CreateExe; #[cfg(feature = "wast")] use crate::commands::Wast; use crate::commands::{ - Add, Cache, Config, Init, Inspect, List, Login, Publish, Run, RunUnstable, SelfUpdate, - Validate, Whoami, + Add, Cache, Config, Init, Inspect, List, Login, Publish, Run, SelfUpdate, Validate, Whoami, }; #[cfg(feature = "static-artifact-create")] use crate::commands::{CreateObj, GenCHeader}; @@ -41,9 +40,6 @@ enum WasmerCLIOptions { /// List all locally installed packages List(List), - /// Run a WebAssembly file. Formats accepted: wasm, wat - Run(Run), - /// Login into a wapm.io-like registry Login(Login), @@ -163,7 +159,8 @@ enum WasmerCLIOptions { Add(Add), /// (unstable) Run a WebAssembly file or WEBC container. - RunUnstable(RunUnstable), + #[clap(alias = "run-unstable")] + Run(Run), // DEPLOY commands #[clap(subcommand)] @@ -202,7 +199,6 @@ impl WasmerCLIOptions { Self::Binfmt(binfmt) => binfmt.execute(), Self::Whoami(whoami) => whoami.execute(), Self::Add(install) => install.execute(), - Self::RunUnstable(run2) => run2.execute(), // Deploy commands. Self::App(apps) => apps.run(), diff --git a/lib/cli/src/commands.rs b/lib/cli/src/commands.rs index 337d825ed8c..a86d8aa56bf 100644 --- a/lib/cli/src/commands.rs +++ b/lib/cli/src/commands.rs @@ -18,7 +18,6 @@ mod list; mod login; mod publish; mod run; -mod run_unstable; mod self_update; mod validate; #[cfg(feature = "wast")] @@ -34,8 +33,8 @@ pub use create_exe::*; #[cfg(feature = "wast")] pub use wast::*; pub use { - add::*, cache::*, config::*, init::*, inspect::*, list::*, login::*, publish::*, run::*, - run_unstable::RunUnstable, self_update::*, validate::*, whoami::*, + add::*, cache::*, config::*, init::*, inspect::*, list::*, login::*, publish::*, run::Run, + self_update::*, validate::*, whoami::*, }; #[cfg(feature = "static-artifact-create")] pub use {create_obj::*, gen_c_header::*}; diff --git a/lib/cli/src/commands/run.rs b/lib/cli/src/commands/run.rs index 5ab626cb7d7..1faea65462a 100644 --- a/lib/cli/src/commands/run.rs +++ b/lib/cli/src/commands/run.rs @@ -1,772 +1,674 @@ -use crate::commands::run::wasi::RunProperties; -use crate::common::get_cache_dir; -use crate::logging; -use crate::package_source::PackageSource; -use crate::store::{CompilerType, StoreOptions}; -use crate::suggestions::suggest_function_exports; -use crate::warning; -use anyhow::{anyhow, Context, Result}; -use clap::Parser; -#[cfg(feature = "coredump")] -use std::fs::File; -use std::net::SocketAddr; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::{io::Write, sync::Arc}; -use tokio::runtime::Handle; -use wasmer::FunctionEnv; -use wasmer::*; -use wasmer_cache::{Cache, FileSystemCache, Hash}; -use wasmer_registry::WasmerConfig; -use wasmer_types::Type as ValueType; -use wasmer_wasix::runners::Runner; +#![allow(missing_docs, unused)] mod wasi; -pub(crate) use wasi::Wasi; - -/// The options for the `wasmer run` subcommand, runs either a package, URL or a file -#[derive(Debug, Parser, Clone)] +use std::{ + collections::BTreeMap, + fmt::{Binary, Display}, + fs::File, + io::{ErrorKind, LineWriter, Read, Write}, + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, + sync::{Arc, Mutex}, + time::{Duration, SystemTime}, +}; + +use anyhow::{Context, Error}; +use clap::Parser; +use clap_verbosity_flag::WarnLevel; +use once_cell::sync::Lazy; +use sha2::{Digest, Sha256}; +use tempfile::NamedTempFile; +use tokio::runtime::Handle; +use url::Url; +use wapm_targz_to_pirita::{FileMap, TransformManifestFunctions}; +use wasmer::{ + DeserializeError, Engine, Function, Imports, Instance, Module, Store, Type, TypedFunction, + Value, +}; +#[cfg(feature = "compiler")] +use wasmer_compiler::ArtifactBuild; +use wasmer_registry::Package; +use wasmer_wasix::{ + bin_factory::BinaryPackage, + runners::{MappedDirectory, Runner}, + runtime::resolver::PackageSpecifier, + WasiError, +}; +use wasmer_wasix::{ + runners::{ + emscripten::EmscriptenRunner, + wasi::WasiRunner, + wcgi::{AbortHandle, WcgiRunner}, + }, + Runtime, +}; +use webc::{metadata::Manifest, v1::DirOrFile, Container}; + +use crate::{commands::run::wasi::Wasi, error::PrettyError, store::StoreOptions}; + +static WASMER_HOME: Lazy = Lazy::new(|| { + wasmer_registry::WasmerConfig::get_wasmer_dir() + .ok() + .or_else(|| dirs::home_dir().map(|home| home.join(".wasmer"))) + .unwrap_or_else(|| PathBuf::from(".wasmer")) +}); + +/// The unstable `wasmer run` subcommand. +#[derive(Debug, Parser)] pub struct Run { - /// File to run - #[clap(name = "SOURCE")] - pub(crate) path: PackageSource, - /// Options to run the file / package / URL with #[clap(flatten)] - pub(crate) options: RunWithoutFile, -} - -/// Same as `wasmer run`, but without the required `path` argument (injected previously) -#[derive(Debug, Parser, Clone, Default)] -pub struct RunWithoutFile { - /// When installing packages with `wasmer $package`, force re-downloading the package - #[clap(long = "force", short = 'f')] - pub(crate) force_install: bool, - - /// Disable the cache - #[clap(long = "disable-cache")] - pub(crate) disable_cache: bool, - - /// Invoke a specified function - #[clap(long = "invoke", short = 'i')] - pub(crate) invoke: Option, - - /// The command name is a string that will override the first argument passed - /// to the wasm program. This is used in wapm to provide nicer output in - /// help commands and error messages of the running wasm program - #[clap(long = "command-name", hide = true)] - pub(crate) command_name: Option, - - /// A prehashed string, used to speed up start times by avoiding hashing the - /// wasm module. If the specified hash is not found, Wasmer will hash the module - /// as if no `cache-key` argument was passed. - #[clap(long = "cache-key", hide = true)] - pub(crate) cache_key: Option, - + verbosity: clap_verbosity_flag::Verbosity, + /// The Wasmer home directory. + #[clap(long = "wasmer-dir", env = "WASMER_DIR", default_value = WASMER_HOME.as_os_str())] + wasmer_dir: PathBuf, #[clap(flatten)] - pub(crate) store: StoreOptions, - - // TODO: refactor WASI structure to allow shared options with Emscripten + store: StoreOptions, #[clap(flatten)] - pub(crate) wasi: Wasi, - - /// Enable non-standard experimental IO devices - #[cfg(feature = "io-devices")] - #[clap(long = "enable-io-devices")] - pub(crate) enable_experimental_io_devices: bool, - - /// Enable debug output - #[clap(long = "debug", short = 'd')] - pub(crate) debug: bool, - - #[clap(long = "verbose")] - pub(crate) verbose: Option, - + wasi: crate::commands::run::Wasi, #[clap(flatten)] - pub(crate) wcgi: WcgiOptions, - - /// Enable coredump generation after a WebAssembly trap. - #[clap(name = "COREDUMP PATH", long = "coredump-on-trap")] - coredump_on_trap: Option, - - #[cfg(feature = "sys")] - /// The stack size (default is 1048576) + wcgi: WcgiOptions, + /// Set the default stack size (default is 1048576) #[clap(long = "stack-size")] - pub(crate) stack_size: Option, - - /// Application arguments - #[clap(value_name = "ARGS")] - pub(crate) args: Vec, -} - -/// Same as `Run`, but uses a resolved local file path. -#[derive(Debug, Clone, Default)] -pub struct RunWithPathBuf { - /// File to run - pub(crate) path: PathBuf, - /// Options for running the file - pub(crate) options: RunWithoutFile, + stack_size: Option, + /// The function or command to invoke. + #[clap(short, long, aliases = &["command", "invoke", "command-name"])] + entrypoint: Option, + /// Generate a coredump at this path if a WebAssembly trap occurs + #[clap(name = "COREDUMP PATH", long)] + coredump_on_trap: Option, + /// The file, URL, or package to run. + #[clap(value_parser = PackageSource::infer)] + input: PackageSource, + /// Command-line arguments passed to the package + args: Vec, } -impl Deref for RunWithPathBuf { - type Target = RunWithoutFile; - fn deref(&self) -> &Self::Target { - &self.options +impl Run { + pub fn execute(&self) -> ! { + let result = self.execute_inner(); + exit_with_wasi_exit_code(result); } -} -impl RunWithPathBuf { - /// Execute the run command - pub fn execute(&self) -> Result<()> { - let mut self_clone = self.clone(); - - if self_clone.path.is_dir() { - let (manifest, pathbuf) = wasmer_registry::get_executable_file_from_path( - &self_clone.path, - self_clone.command_name.as_deref(), - )?; - - // See https://github.com/wasmerio/wasmer/issues/3492 - - // we need IndexMap to have a stable ordering for the [fs] mapping, - // otherwise overlapping filesystem mappings might not work - // since we want to control the order of mounting directories from the - // wasmer.toml file - let default = indexmap::IndexMap::default(); - let fs = manifest.fs.as_ref().unwrap_or(&default); - for (alias, real_dir) in fs.iter() { - let real_dir = self_clone.path.join(real_dir); - if !real_dir.exists() { - #[cfg(feature = "debug")] - if self_clone.debug { - println!( - "warning: cannot map {alias:?} to {}: directory does not exist", - real_dir.display() - ); - } - continue; - } + fn execute_inner(&self) -> Result<(), Error> { + crate::logging::set_up_logging(self.verbosity.log_level_filter()); + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + let handle = runtime.handle().clone(); - self_clone.options.wasi.map_dir(alias, real_dir.clone()); + #[cfg(feature = "sys")] + if self.stack_size.is_some() { + wasmer_vm::set_stack_size(self.stack_size.unwrap()); + } + + let (store, _) = self.store.get_store()?; + let runtime = + self.wasi + .prepare_runtime(store.engine().clone(), &self.wasmer_dir, handle)?; + + let target = self + .input + .resolve_target(&runtime) + .with_context(|| format!("Unable to resolve \"{}\"", self.input))?; + + let runtime: Arc = Arc::new(runtime); + let result = { + match target { + ExecutableTarget::WebAssembly { module, path } => { + self.execute_wasm(&path, &module, store, runtime) + } + ExecutableTarget::Package(pkg) => self.execute_webc(&pkg, runtime), } + }; - self_clone.path = pathbuf; + if let Err(e) = &result { + self.maybe_save_coredump(e); } - if self.debug { - let level = match self_clone.verbose { - Some(1) => log::LevelFilter::Debug, - Some(_) | None => log::LevelFilter::Trace, - }; - logging::set_up_logging(level); - } + result + } - let invoke_res = self_clone.inner_execute().with_context(|| { - format!( - "failed to run `{}`{}", - self_clone.path.display(), - if CompilerType::enabled().is_empty() { - " (no compilers enabled)" - } else { - "" - } - ) - }); - - if let Err(err) = invoke_res { - #[cfg(feature = "coredump")] - if let Some(coredump_path) = self.coredump_on_trap.as_ref() { - let source_name = self.path.to_str().unwrap_or("unknown"); - if let Err(coredump_err) = generate_coredump(&err, source_name, coredump_path) { - eprintln!("warning: coredump failed to generate: {}", coredump_err); - Err(err) - } else { - Err(err.context(format!("core dumped at {}", coredump_path.display()))) - } - } else { - Err(err) + fn execute_target( + &self, + executable_target: ExecutableTarget, + runtime: Arc, + store: Store, + ) -> Result<(), Error> { + match executable_target { + ExecutableTarget::WebAssembly { module, path } => { + self.execute_wasm(&path, &module, store, runtime) } + ExecutableTarget::Package(pkg) => self.execute_webc(&pkg, runtime), + } + } - #[cfg(not(feature = "coredump"))] - Err(err) + #[tracing::instrument(skip_all)] + fn execute_wasm( + &self, + path: &Path, + module: &Module, + mut store: Store, + runtime: Arc, + ) -> Result<(), Error> { + if wasmer_emscripten::is_emscripten_module(module) { + self.execute_emscripten_module() + } else if wasmer_wasix::is_wasi_module(module) || wasmer_wasix::is_wasix_module(module) { + self.execute_wasi_module(path, module, runtime, store) } else { - invoke_res + self.execute_pure_wasm_module(module, &mut store) } } - fn inner_module_init(&self, store: &mut Store, instance: &Instance) -> Result<()> { - #[cfg(feature = "sys")] - if self.stack_size.is_some() { - wasmer_vm::set_stack_size(self.stack_size.unwrap()); + #[tracing::instrument(skip_all)] + fn execute_webc( + &self, + pkg: &BinaryPackage, + runtime: Arc, + ) -> Result<(), Error> { + let id = match self.entrypoint.as_deref() { + Some(cmd) => cmd, + None => infer_webc_entrypoint(pkg)?, + }; + let cmd = pkg + .get_command(id) + .with_context(|| format!("Unable to get metadata for the \"{id}\" command"))?; + + let uses = self.load_injected_packages(&*runtime)?; + + if WcgiRunner::can_run_command(cmd.metadata())? { + self.run_wcgi(id, pkg, uses, runtime) + } else if WasiRunner::can_run_command(cmd.metadata())? { + self.run_wasi(id, pkg, uses, runtime) + } else if EmscriptenRunner::can_run_command(cmd.metadata())? { + self.run_emscripten(id, pkg, runtime) + } else { + anyhow::bail!( + "Unable to find a runner that supports \"{}\"", + cmd.metadata().runner + ); } + } - // If this module exports an _initialize function, run that first. - if let Ok(initialize) = instance.exports.get_function("_initialize") { - initialize - .call(store, &[]) - .with_context(|| "failed to run _initialize function")?; + #[tracing::instrument(level = "debug", skip_all)] + fn load_injected_packages(&self, runtime: &dyn Runtime) -> Result, Error> { + let mut dependencies = Vec::new(); + + for name in &self.wasi.uses { + let specifier = PackageSpecifier::parse(name) + .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?; + let pkg = runtime + .task_manager() + .block_on(BinaryPackage::from_registry(&specifier, runtime)) + .with_context(|| format!("Unable to load \"{name}\""))?; + dependencies.push(pkg); } - Ok(()) + + Ok(dependencies) } - fn inner_module_invoke_function( - store: &mut Store, - instance: &Instance, - path: &Path, - invoke: &str, - args: &[String], - ) -> Result<()> { - let result = Self::invoke_function(store, instance, path, invoke, args)?; + fn run_wasi( + &self, + command_name: &str, + pkg: &BinaryPackage, + uses: Vec, + runtime: Arc, + ) -> Result<(), Error> { + let mut runner = wasmer_wasix::runners::wasi::WasiRunner::new() + .with_args(self.args.clone()) + .with_envs(self.wasi.env_vars.clone()) + .with_mapped_directories(self.wasi.mapped_dirs.clone()) + .with_injected_packages(uses); + if self.wasi.forward_host_env { + runner.set_forward_host_env(); + } + + *runner.capabilities() = self.wasi.capabilities(); + + runner.run_command(command_name, pkg, runtime) + } + + fn run_wcgi( + &self, + command_name: &str, + pkg: &BinaryPackage, + uses: Vec, + runtime: Arc, + ) -> Result<(), Error> { + let mut runner = wasmer_wasix::runners::wcgi::WcgiRunner::new(); + + runner + .config() + .args(self.args.clone()) + .addr(self.wcgi.addr) + .envs(self.wasi.env_vars.clone()) + .map_directories(self.wasi.mapped_dirs.clone()) + .callbacks(Callbacks::new(self.wcgi.addr)) + .inject_packages(uses); + *runner.config().capabilities() = self.wasi.capabilities(); + if self.wasi.forward_host_env { + runner.config().forward_host_env(); + } + + runner.run_command(command_name, pkg, runtime) + } + + fn run_emscripten( + &self, + command_name: &str, + pkg: &BinaryPackage, + runtime: Arc, + ) -> Result<(), Error> { + let mut runner = wasmer_wasix::runners::emscripten::EmscriptenRunner::new(); + runner.set_args(self.args.clone()); + + runner.run_command(command_name, pkg, runtime) + } + + #[tracing::instrument(skip_all)] + fn execute_pure_wasm_module(&self, module: &Module, store: &mut Store) -> Result<(), Error> { + let imports = Imports::default(); + let instance = Instance::new(store, module, &imports) + .context("Unable to instantiate the WebAssembly module")?; + + let entrypoint = match &self.entrypoint { + Some(entry) => { + instance.exports + .get_function(entry) + .with_context(|| format!("The module doesn't contain a \"{entry}\" function"))? + }, + None => { + instance.exports.get_function("_start") + .context("The module doesn't contain a \"_start\" function. Either implement it or specify an entrypoint function.")? + } + }; + + let return_values = invoke_function(&instance, store, entrypoint, &self.args)?; + println!( "{}", - result + return_values .iter() .map(|val| val.to_string()) .collect::>() .join(" ") ); + Ok(()) } - fn inner_module_run(&self, store: &mut Store, instance: &Instance) -> Result { - // Do we want to invoke a function? - if let Some(ref invoke) = self.invoke { - Self::inner_module_invoke_function( - store, - instance, - self.path.as_path(), - invoke, - &self.args, - )?; - } else { - let start: Function = - Self::try_find_function(instance, self.path.as_path(), "_start", &[])?; - start.call(store, &[])?; - } + #[tracing::instrument(skip_all)] + fn execute_wasi_module( + &self, + wasm_path: &Path, + module: &Module, + runtime: Arc, + store: Store, + ) -> Result<(), Error> { + let program_name = wasm_path.display().to_string(); + + let builder = self + .wasi + .prepare(module, program_name, self.args.clone(), runtime)?; + + builder.run_with_store_async(module.clone(), store)?; - Ok(0) + Ok(()) } - fn inner_execute(&self) -> Result<()> { - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()?; - let handle = runtime.handle().clone(); + #[tracing::instrument(skip_all)] + fn execute_emscripten_module(&self) -> Result<(), Error> { + anyhow::bail!("Emscripten packages are not currently supported") + } - #[cfg(feature = "webc_runner")] - { - if let Ok(pf) = webc::Container::from_disk(self.path.clone()) { - return self.run_container(pf, self.command_name.as_deref(), &self.args, handle); - } - } - let (mut store, module) = self.get_store_module()?; - #[cfg(feature = "emscripten")] - { - use wasmer_emscripten::{ - generate_emscripten_env, is_emscripten_module, run_emscripten_instance, EmEnv, - EmscriptenGlobals, - }; - // TODO: refactor this - if is_emscripten_module(&module) { - let em_env = EmEnv::new(); - for (k, v) in self.wasi.env_vars.iter() { - em_env.set_env_var(k, v); - } - // create an EmEnv with default global - let env = FunctionEnv::new(&mut store, em_env); - let mut emscripten_globals = EmscriptenGlobals::new(&mut store, &env, &module) - .map_err(|e| anyhow!("{}", e))?; - env.as_mut(&mut store).set_data( - &emscripten_globals.data, - self.wasi - .mapped_dirs - .clone() - .into_iter() - .map(|d| (d.guest, d.host)) - .collect(), + #[allow(unused_variables)] + fn maybe_save_coredump(&self, e: &Error) { + #[cfg(feature = "coredump")] + if let Some(coredump) = &self.coredump_on_trap { + if let Err(e) = generate_coredump(e, self.input.to_string(), coredump) { + tracing::warn!( + error = &*e as &dyn std::error::Error, + coredump_path=%coredump.display(), + "Unable to generate a coredump", ); - let import_object = - generate_emscripten_env(&mut store, &env, &mut emscripten_globals); - let mut instance = match Instance::new(&mut store, &module, &import_object) { - Ok(instance) => instance, - Err(e) => { - let err: Result<(), _> = Err(e); - #[cfg(feature = "wasi")] - { - if Wasi::has_wasi_imports(&module) { - return err.with_context(|| "This module has both Emscripten and WASI imports. Wasmer does not currently support Emscripten modules using WASI imports."); - } - } - return err.with_context(|| "Can't instantiate emscripten module"); - } - }; - - run_emscripten_instance( - &mut instance, - env.into_mut(&mut store), - &mut emscripten_globals, - if let Some(cn) = &self.command_name { - cn - } else { - self.path.to_str().unwrap() - }, - self.args.iter().map(|arg| arg.as_str()).collect(), - self.invoke.clone(), - )?; - return Ok(()); } } + } - // If WASI is enabled, try to execute it with it - #[cfg(feature = "wasi")] - let ret = { - use std::collections::BTreeSet; - use wasmer_wasix::WasiVersion; - - let wasi_versions = Wasi::get_versions(&module); - match wasi_versions { - Some(wasi_versions) if !wasi_versions.is_empty() => { - if wasi_versions.len() >= 2 { - let get_version_list = |versions: &BTreeSet| -> String { - versions - .iter() - .map(|v| format!("`{}`", v.get_namespace_str())) - .collect::>() - .join(", ") - }; - if self.wasi.deny_multiple_wasi_versions { - let version_list = get_version_list(&wasi_versions); - bail!("Found more than 1 WASI version in this module ({}) and `--deny-multiple-wasi-versions` is enabled.", version_list); - } - } - - let program_name = self - .command_name - .clone() - .or_else(|| { - self.path - .file_name() - .map(|f| f.to_string_lossy().to_string()) - }) - .unwrap_or_default(); - - let wasmer_dir = WasmerConfig::get_wasmer_dir().map_err(anyhow::Error::msg)?; - let runtime = Arc::new(self.wasi.prepare_runtime(store.engine().clone(), &wasmer_dir, handle)?); - - let (ctx, instance) = self - .wasi - .instantiate(&module, program_name, self.args.clone(), runtime, &mut store) - .with_context(|| "failed to instantiate WASI module")?; - - let capable_of_deep_sleep = unsafe { ctx.data(&store).capable_of_deep_sleep() }; - ctx.data_mut(&mut store) - .enable_deep_sleep = capable_of_deep_sleep; - - self.inner_module_init(&mut store, &instance)?; - Wasi::run( - RunProperties { - ctx, path: self.path.clone(), invoke: self.invoke.clone(), args: self.args.clone() - }, - store - ) - } - // not WASI - _ => { - let instance = Instance::new(&mut store, &module, &imports! {})?; - self.inner_module_init(&mut store, &instance)?; - self.inner_module_run(&mut store, &instance) - } - } - }.map(|exit_code| { - std::io::stdout().flush().ok(); - std::io::stderr().flush().ok(); - std::process::exit(exit_code); - }); - - #[cfg(not(feature = "wasi"))] - let ret = { - let instance = Instance::new(&module, &imports! {})?; - - // If this module exports an _initialize function, run that first. - if let Ok(initialize) = instance.exports.get_function("_initialize") { - initialize - .call(&[]) - .with_context(|| "failed to run _initialize function")?; - } + /// Create Run instance for arguments/env, assuming we're being run from a + /// CFP binfmt interpreter. + pub fn from_binfmt_args() -> Self { + Run::from_binfmt_args_fallible().unwrap_or_else(|e| { + crate::error::PrettyError::report::<()>( + Err(e).context("Failed to set up wasmer binfmt invocation"), + ) + }) + } - // Do we want to invoke a function? - if let Some(ref invoke) = self.invoke { - let result = - Self::invoke_function(&instance, self.path.as_path(), invoke, &self.args)?; - println!( - "{}", - result - .iter() - .map(|val| val.to_string()) - .collect::>() - .join(" ") - ); - } else { - let start: Function = - Self.try_find_function(&instance, self.path.as_path(), "_start", &[])?; - let result = start.call(&[]); - #[cfg(feature = "wasi")] - self.wasi.handle_result(result)?; - #[cfg(not(feature = "wasi"))] - result?; + fn from_binfmt_args_fallible() -> Result { + if !cfg!(linux) { + anyhow::bail!("binfmt_misc is only available on linux."); + } + + let argv = std::env::args().collect::>(); + let (_interpreter, executable, original_executable, args) = match &argv[..] { + [a, b, c, rest @ ..] => (a, b, c, rest), + _ => { + bail!("Wasmer binfmt interpreter needs at least three arguments (including $0) - must be registered as binfmt interpreter with the CFP flags. (Got arguments: {:?})", argv); } }; - - ret + let store = StoreOptions::default(); + Ok(Run { + verbosity: clap_verbosity_flag::Verbosity::new(0, 0), + wasmer_dir: WASMER_HOME.clone(), + store, + wasi: Wasi::for_binfmt_interpreter()?, + wcgi: WcgiOptions::default(), + stack_size: None, + entrypoint: Some(original_executable.to_string()), + coredump_on_trap: None, + input: PackageSource::infer(executable)?, + args: args.to_vec(), + }) } +} - #[cfg(feature = "webc_runner")] - fn run_container( - &self, - container: webc::Container, - id: Option<&str>, - args: &[String], - handle: Handle, - ) -> Result<(), anyhow::Error> { - use wasmer_wasix::{ - bin_factory::BinaryPackage, - runners::{emscripten::EmscriptenRunner, wasi::WasiRunner, wcgi::WcgiRunner}, - Runtime, - }; +fn invoke_function( + instance: &Instance, + store: &mut Store, + func: &Function, + args: &[String], +) -> Result, Error> { + let func_ty = func.ty(store); + let required_arguments = func_ty.params().len(); + let provided_arguments = args.len(); + + anyhow::ensure!( + required_arguments == provided_arguments, + "Function expected {} arguments, but received {}", + required_arguments, + provided_arguments, + ); + + let invoke_args = args + .iter() + .zip(func_ty.params().iter()) + .map(|(arg, param_type)| { + parse_value(arg, *param_type) + .with_context(|| format!("Unable to convert {arg:?} to {param_type:?}")) + }) + .collect::, _>>()?; - let wasmer_dir = WasmerConfig::get_wasmer_dir().map_err(anyhow::Error::msg)?; + let return_values = func.call(store, &invoke_args)?; - let id = id - .or_else(|| container.manifest().entrypoint.as_deref()) - .context("No command specified")?; - let command = container - .manifest() - .commands - .get(id) - .with_context(|| format!("No metadata found for the command, \"{id}\""))?; + Ok(return_values) +} - let (store, _compiler_type) = self.store.get_store()?; - let runtime = self - .wasi - .prepare_runtime(store.engine().clone(), &wasmer_dir, handle)?; - let runtime = Arc::new(runtime); - let pkg = runtime - .task_manager() - .block_on(BinaryPackage::from_webc(&container, &*runtime))?; - - if WasiRunner::can_run_command(command).unwrap_or(false) { - let mut runner = WasiRunner::new(); - runner.set_args(args.to_vec()); - return runner - .run_command(id, &pkg, runtime) - .context("WASI runner failed"); +fn parse_value(s: &str, ty: wasmer_types::Type) -> Result { + let value = match ty { + Type::I32 => Value::I32(s.parse()?), + Type::I64 => Value::I64(s.parse()?), + Type::F32 => Value::F32(s.parse()?), + Type::F64 => Value::F64(s.parse()?), + Type::V128 => Value::V128(s.parse()?), + _ => anyhow::bail!("There is no known conversion from {s:?} to {ty:?}"), + }; + Ok(value) +} + +fn infer_webc_entrypoint(pkg: &BinaryPackage) -> Result<&str, Error> { + if let Some(entrypoint) = pkg.entrypoint_cmd.as_deref() { + return Ok(entrypoint); + } + + match pkg.commands.as_slice() { + [] => anyhow::bail!("The WEBC file doesn't contain any executable commands"), + [one] => Ok(one.name()), + [..] => { + let mut commands: Vec<_> = pkg.commands.iter().map(|cmd| cmd.name()).collect(); + commands.sort(); + anyhow::bail!( + "Unable to determine the WEBC file's entrypoint. Please choose one of {:?}", + commands, + ); } + } +} + +/// The input that was passed in via the command-line. +#[derive(Debug, Clone, PartialEq)] +enum PackageSource { + /// A file on disk (`*.wasm`, `*.webc`, etc.). + File(PathBuf), + /// A directory containing a `wasmer.toml` file + Dir(PathBuf), + /// A package to be downloaded (a URL, package name, etc.) + Package(PackageSpecifier), +} - if EmscriptenRunner::can_run_command(command).unwrap_or(false) { - let mut runner = EmscriptenRunner::new(); - runner.set_args(args.to_vec()); - return runner - .run_command(id, &pkg, runtime) - .context("Emscripten runner failed"); +impl PackageSource { + fn infer(s: &str) -> Result { + let path = Path::new(s); + if path.is_file() { + return Ok(PackageSource::File(path.to_path_buf())); + } else if path.is_dir() { + return Ok(PackageSource::Dir(path.to_path_buf())); } - if WcgiRunner::can_run_command(command).unwrap_or(false) { - let mut runner = WcgiRunner::new(); - runner - .config() - .args(args) - .addr(self.wcgi.addr) - .envs(self.wasi.env_vars.clone()) - .map_directories(self.wasi.mapped_dirs.clone()); - if self.wasi.forward_host_env { - runner.config().forward_host_env(); - } + if let Ok(pkg) = PackageSpecifier::parse(s) { + return Ok(PackageSource::Package(pkg)); + } - return runner - .run_command(id, &pkg, runtime) - .context("WCGI runner failed"); + Err(anyhow::anyhow!( + "Unable to resolve \"{s}\" as a URL, package name, or file on disk" + )) + } + + /// Try to resolve the [`PackageSource`] to an executable artifact. + /// + /// This will try to automatically download and cache any resources from the + /// internet. + fn resolve_target(&self, rt: &dyn Runtime) -> Result { + match self { + PackageSource::File(path) => ExecutableTarget::from_file(path, rt), + PackageSource::Dir(d) => ExecutableTarget::from_dir(d, rt), + PackageSource::Package(pkg) => { + let pkg = rt + .task_manager() + .block_on(BinaryPackage::from_registry(pkg, rt))?; + Ok(ExecutableTarget::Package(pkg)) + } } + } +} - anyhow::bail!( - "Unable to find a runner that supports \"{}\"", - command.runner - ); +impl Display for PackageSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PackageSource::File(path) | PackageSource::Dir(path) => write!(f, "{}", path.display()), + PackageSource::Package(p) => write!(f, "{p}"), + } } +} + +/// We've been given the path for a file... What does it contain and how should +/// that be run? +#[derive(Debug, Clone)] +enum TargetOnDisk { + WebAssemblyBinary, + Wat, + LocalWebc, + Artifact, +} + +impl TargetOnDisk { + fn from_file(path: &Path) -> Result { + // Normally the first couple hundred bytes is enough to figure + // out what type of file this is. + let mut buffer = [0_u8; 512]; - fn get_store_module(&self) -> Result<(Store, Module)> { - let contents = std::fs::read(self.path.clone())?; - #[cfg(not(feature = "jsc"))] - if wasmer_compiler::Artifact::is_deserializable(&contents) { - let engine = wasmer_compiler::EngineBuilder::headless(); - let store = Store::new(engine); - let module = unsafe { Module::deserialize_from_file(&store, &self.path)? }; - return Ok((store, module)); + let mut f = File::open(path) + .with_context(|| format!("Unable to open \"{}\" for reading", path.display(),))?; + let bytes_read = f.read(&mut buffer)?; + + let leading_bytes = &buffer[..bytes_read]; + + if wasmer::is_wasm(leading_bytes) { + return Ok(TargetOnDisk::WebAssemblyBinary); } - let (store, compiler_type) = self.store.get_store()?; - #[cfg(feature = "cache")] - let module_result: Result = if !self.disable_cache && contents.len() > 0x1000 { - self.get_module_from_cache(&store, &contents, &compiler_type) - } else { - Module::new(&store, contents).map_err(|e| e.into()) - }; - #[cfg(not(feature = "cache"))] - let module_result = Module::new(&store, &contents); - let mut module = module_result.with_context(|| { - format!( - "module instantiation failed (compiler: {})", - compiler_type.to_string() - ) - })?; - // We set the name outside the cache, to make sure we dont cache the name - module.set_name(&self.path.file_name().unwrap_or_default().to_string_lossy()); + if webc::detect(leading_bytes).is_ok() { + return Ok(TargetOnDisk::LocalWebc); + } - Ok((store, module)) - } + #[cfg(feature = "compiler")] + if ArtifactBuild::is_deserializable(leading_bytes) { + return Ok(TargetOnDisk::Artifact); + } - #[cfg(feature = "cache")] - fn get_module_from_cache( - &self, - store: &Store, - contents: &[u8], - compiler_type: &CompilerType, - ) -> Result { - // We try to get it from cache, in case caching is enabled - // and the file length is greater than 4KB. - // For files smaller than 4KB caching is not worth, - // as it takes space and the speedup is minimal. - let mut cache = self.get_cache(compiler_type)?; - // Try to get the hash from the provided `--cache-key`, otherwise - // generate one from the provided file `.wasm` contents. - let hash = self - .cache_key - .as_ref() - .and_then(|key| Hash::from_str(key).ok()) - .unwrap_or_else(|| Hash::generate(contents)); - match unsafe { cache.load(store, hash) } { - Ok(module) => Ok(module), - Err(e) => { - match e { - DeserializeError::Io(_) => { - // Do not notify on IO errors - } - err => { - warning!("cached module is corrupted: {}", err); - } - } - let module = Module::new(store, contents)?; - // Store the compiled Module in cache - cache.store(hash, &module)?; - Ok(module) - } + // If we can't figure out the file type based on its content, fall back + // to checking the extension. + + match path.extension().and_then(|s| s.to_str()) { + Some("wat") => Ok(TargetOnDisk::Wat), + Some("wasm") => Ok(TargetOnDisk::WebAssemblyBinary), + Some("webc") => Ok(TargetOnDisk::LocalWebc), + Some("wasmu") => Ok(TargetOnDisk::WebAssemblyBinary), + _ => anyhow::bail!("Unable to determine how to execute \"{}\"", path.display()), } } +} - #[cfg(feature = "cache")] - /// Get the Compiler Filesystem cache - fn get_cache(&self, compiler_type: &CompilerType) -> Result { - let mut cache_dir_root = get_cache_dir(); - cache_dir_root.push(compiler_type.to_string()); - let mut cache = FileSystemCache::new(cache_dir_root)?; +#[derive(Debug, Clone)] +enum ExecutableTarget { + WebAssembly { module: Module, path: PathBuf }, + Package(BinaryPackage), +} - let extension = "wasmu"; - cache.set_cache_extension(Some(extension)); - Ok(cache) - } +impl ExecutableTarget { + /// Try to load a Wasmer package from a directory containing a `wasmer.toml` + /// file. + fn from_dir(dir: &Path, runtime: &dyn Runtime) -> Result { + let webc = construct_webc_in_memory(dir)?; + let container = Container::from_bytes(webc)?; - fn try_find_function( - instance: &Instance, - path: &Path, - name: &str, - args: &[String], - ) -> Result { - Ok(instance - .exports - .get_function(name) - .map_err(|e| { - if instance.module().info().functions.is_empty() { - anyhow!("The module has no exported functions to call.") - } else { - let suggested_functions = suggest_function_exports(instance.module(), ""); - let names = suggested_functions - .iter() - .take(3) - .map(|arg| format!("`{}`", arg)) - .collect::>() - .join(", "); - let suggested_command = format!( - "wasmer {} -i {} {}", - path.display(), - suggested_functions.get(0).unwrap_or(&String::new()), - args.join(" ") - ); - let suggestion = if suggested_functions.is_empty() { - String::from("Can not find any export functions.") - } else { - format!( - "Similar functions found: {}.\nTry with: {}", - names, suggested_command - ) - }; - match e { - ExportError::Missing(_) => { - anyhow!("No export `{}` found in the module.\n{}", name, suggestion) - } - ExportError::IncompatibleType => anyhow!( - "Export `{}` found, but is not a function.\n{}", - name, - suggestion - ), - } - } - })? - .clone()) + let pkg = runtime + .task_manager() + .block_on(BinaryPackage::from_webc(&container, runtime))?; + + Ok(ExecutableTarget::Package(pkg)) } - fn invoke_function( - ctx: &mut impl AsStoreMut, - instance: &Instance, - path: &Path, - invoke: &str, - args: &[String], - ) -> Result> { - let func: Function = Self::try_find_function(instance, path, invoke, args)?; - let func_ty = func.ty(ctx); - let required_arguments = func_ty.params().len(); - let provided_arguments = args.len(); - if required_arguments != provided_arguments { - bail!( - "Function expected {} arguments, but received {}: \"{}\"", - required_arguments, - provided_arguments, - args.join(" ") - ); + /// Try to load a file into something that can be used to run it. + #[tracing::instrument(skip_all)] + fn from_file(path: &Path, runtime: &dyn Runtime) -> Result { + match TargetOnDisk::from_file(path)? { + TargetOnDisk::WebAssemblyBinary | TargetOnDisk::Wat => { + let wasm = std::fs::read(path)?; + let engine = runtime.engine().context("No engine available")?; + let module = Module::new(&engine, wasm)?; + Ok(ExecutableTarget::WebAssembly { + module, + path: path.to_path_buf(), + }) + } + TargetOnDisk::Artifact => { + let engine = runtime.engine().context("No engine available")?; + let module = unsafe { Module::deserialize_from_file(&engine, path)? }; + + Ok(ExecutableTarget::WebAssembly { + module, + path: path.to_path_buf(), + }) + } + TargetOnDisk::LocalWebc => { + let container = Container::from_disk(path)?; + let pkg = runtime + .task_manager() + .block_on(BinaryPackage::from_webc(&container, runtime))?; + Ok(ExecutableTarget::Package(pkg)) + } } - let invoke_args = args - .iter() - .zip(func_ty.params().iter()) - .map(|(arg, param_type)| match param_type { - ValueType::I32 => { - Ok(Value::I32(arg.parse().map_err(|_| { - anyhow!("Can't convert `{}` into a i32", arg) - })?)) - } - ValueType::I64 => { - Ok(Value::I64(arg.parse().map_err(|_| { - anyhow!("Can't convert `{}` into a i64", arg) - })?)) - } - ValueType::F32 => { - Ok(Value::F32(arg.parse().map_err(|_| { - anyhow!("Can't convert `{}` into a f32", arg) - })?)) - } - ValueType::F64 => { - Ok(Value::F64(arg.parse().map_err(|_| { - anyhow!("Can't convert `{}` into a f64", arg) - })?)) - } - _ => Err(anyhow!( - "Don't know how to convert {} into {:?}", - arg, - param_type - )), - }) - .collect::>>()?; - Ok(func.call(ctx, &invoke_args)?) } } -impl Run { - /// Executes the `wasmer run` command - pub fn execute(&self) -> Result<(), anyhow::Error> { - // downloads and installs the package if necessary - let path_to_run = self.path.download_and_get_filepath()?; - RunWithPathBuf { - path: path_to_run, - options: self.options.clone(), - } - .execute() +#[tracing::instrument(level = "debug", skip_all)] +fn construct_webc_in_memory(dir: &Path) -> Result, Error> { + let mut files = BTreeMap::new(); + load_files_from_disk(&mut files, dir, dir)?; + + let wasmer_toml = DirOrFile::File("wasmer.toml".into()); + if let Some(toml_data) = files.remove(&wasmer_toml) { + // HACK(Michael-F-Bryan): The version of wapm-targz-to-pirita we are + // using doesn't know we renamed "wapm.toml" to "wasmer.toml", so we + // manually patch things up if people have already migrated their + // projects. + files + .entry(DirOrFile::File("wapm.toml".into())) + .or_insert(toml_data); } - /// Create Run instance for arguments/env, - /// assuming we're being run from a CFP binfmt interpreter. - pub fn from_binfmt_args() -> Run { - Self::from_binfmt_args_fallible().unwrap_or_else(|e| { - crate::error::PrettyError::report::<()>( - Err(e).context("Failed to set up wasmer binfmt invocation"), - ) - }) - } + let functions = TransformManifestFunctions::default(); + let webc = wapm_targz_to_pirita::generate_webc_file(files, dir, None, &functions)?; - #[cfg(target_os = "linux")] - fn from_binfmt_args_fallible() -> Result { - let argv = std::env::args().collect::>(); - let (_interpreter, executable, original_executable, args) = match &argv[..] { - [a, b, c, d @ ..] => (a, b, c, d), - _ => { - bail!("Wasmer binfmt interpreter needs at least three arguments (including $0) - must be registered as binfmt interpreter with the CFP flags. (Got arguments: {:?})", argv); - } - }; - let store = StoreOptions::default(); - // TODO: store.compiler.features.all = true; ? - Ok(Self { - // unwrap is safe, since parsing never fails - path: PackageSource::parse(executable).unwrap(), - options: RunWithoutFile { - args: args.to_vec(), - command_name: Some(original_executable.to_string()), - store, - wasi: Wasi::for_binfmt_interpreter()?, - ..Default::default() - }, - }) - } + Ok(webc) +} - #[cfg(not(target_os = "linux"))] - fn from_binfmt_args_fallible() -> Result { - bail!("binfmt_misc is only available on linux.") +fn load_files_from_disk(files: &mut FileMap, dir: &Path, base: &Path) -> Result<(), Error> { + let entries = dir + .read_dir() + .with_context(|| format!("Unable to read the contents of \"{}\"", dir.display()))?; + + for entry in entries { + let path = entry?.path(); + let relative_path = path.strip_prefix(base)?.to_path_buf(); + + if path.is_dir() { + load_files_from_disk(files, &path, base)?; + files.insert(DirOrFile::Dir(relative_path), Vec::new()); + } else if path.is_file() { + let data = std::fs::read(&path) + .with_context(|| format!("Unable to read \"{}\"", path.display()))?; + files.insert(DirOrFile::File(relative_path), data); + } } + Ok(()) } #[cfg(feature = "coredump")] -fn generate_coredump( - err: &anyhow::Error, - source_name: &str, - coredump_path: &PathBuf, -) -> Result<()> { - let err = err - .downcast_ref::() - .ok_or_else(|| anyhow!("no runtime error found to generate coredump with"))?; +fn generate_coredump(err: &Error, source_name: String, coredump_path: &Path) -> Result<(), Error> { + let err: &wasmer::RuntimeError = match err.downcast_ref() { + Some(e) => e, + None => { + log::warn!("no runtime error found to generate coredump with"); + return Ok(()); + } + }; let mut coredump_builder = - wasm_coredump_builder::CoredumpBuilder::new().executable_name(source_name); + wasm_coredump_builder::CoredumpBuilder::new().executable_name(&source_name); - { - let mut thread_builder = wasm_coredump_builder::ThreadBuilder::new().thread_name("main"); + let mut thread_builder = wasm_coredump_builder::ThreadBuilder::new().thread_name("main"); - for frame in err.trace() { - let coredump_frame = wasm_coredump_builder::FrameBuilder::new() - .codeoffset(frame.func_offset() as u32) - .funcidx(frame.func_index()) - .build(); - thread_builder.add_frame(coredump_frame); - } - - coredump_builder.add_thread(thread_builder.build()); + for frame in err.trace() { + let coredump_frame = wasm_coredump_builder::FrameBuilder::new() + .codeoffset(frame.func_offset() as u32) + .funcidx(frame.func_index()) + .build(); + thread_builder.add_frame(coredump_frame); } + coredump_builder.add_thread(thread_builder.build()); + let coredump = coredump_builder .serialize() - .map_err(|err| anyhow!("failed to serialize coredump: {}", err))?; + .map_err(Error::msg) + .context("Coredump serializing failed")?; - let mut f = File::create(coredump_path).context(format!( - "failed to create file at `{}`", - coredump_path.display() - ))?; - f.write_all(&coredump).with_context(|| { + std::fs::write(coredump_path, &coredump).with_context(|| { format!( - "failed to write coredump file at `{}`", + "Unable to save the coredump to \"{}\"", coredump_path.display() ) })?; @@ -788,3 +690,70 @@ impl Default for WcgiOptions { } } } + +#[derive(Debug)] +struct Callbacks { + stderr: Mutex>, + addr: SocketAddr, +} + +impl Callbacks { + fn new(addr: SocketAddr) -> Self { + Callbacks { + stderr: Mutex::new(LineWriter::new(std::io::stderr())), + addr, + } + } +} + +impl wasmer_wasix::runners::wcgi::Callbacks for Callbacks { + fn started(&self, _abort: AbortHandle) { + println!("WCGI Server running at http://{}/", self.addr); + } + + fn on_stderr(&self, raw_message: &[u8]) { + if let Ok(mut stderr) = self.stderr.lock() { + // If the WCGI runner printed any log messages we want to make sure + // they get propagated to the user. Line buffering is important here + // because it helps prevent the output from becoming a complete + // mess. + let _ = stderr.write_all(raw_message); + } + } +} + +/// Exit the current process, using the WASI exit code if the error contains +/// one. +fn exit_with_wasi_exit_code(result: Result<(), Error>) -> ! { + let exit_code = match result { + Ok(_) => 0, + Err(error) => { + match error.chain().find_map(get_exit_code) { + Some(exit_code) => exit_code.raw(), + None => { + eprintln!("{:?}", PrettyError::new(error)); + // Something else happened + 1 + } + } + } + }; + + std::io::stdout().flush().ok(); + std::io::stderr().flush().ok(); + + std::process::exit(exit_code); +} + +fn get_exit_code( + error: &(dyn std::error::Error + 'static), +) -> Option { + if let Some(WasiError::Exit(exit_code)) = error.downcast_ref() { + return Some(*exit_code); + } + if let Some(error) = error.downcast_ref::() { + return error.as_exit_code(); + } + + None +} diff --git a/lib/cli/src/commands/run/wasi.rs b/lib/cli/src/commands/run/wasi.rs index 78fb2567c64..b945351c1f4 100644 --- a/lib/cli/src/commands/run/wasi.rs +++ b/lib/cli/src/commands/run/wasi.rs @@ -13,6 +13,7 @@ use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuild use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value}; use wasmer_wasix::{ bin_factory::BinaryPackage, + capabilities::Capabilities, default_fs_backing, get_wasi_versions, http::HttpClient, os::{tty_sys::SysTty, TtyBridge}, @@ -35,8 +36,6 @@ use wasmer_wasix::{ use crate::utils::{parse_envvar, parse_mapdir}; -use super::RunWithPathBuf; - #[derive(Debug, Parser, Clone, Default)] /// WASI Options pub struct Wasi { @@ -219,15 +218,7 @@ impl Wasi { )? }; - if self.http_client { - let caps = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all(); - builder.capabilities_mut().http_client = caps; - } - - builder - .capabilities_mut() - .threading - .enable_asynchronous_threading = self.enable_async_threads; + *builder.capabilities_mut() = self.capabilities(); #[cfg(feature = "experimental-io-devices")] { @@ -240,6 +231,18 @@ impl Wasi { Ok(builder) } + pub fn capabilities(&self) -> Capabilities { + let mut caps = Capabilities::default(); + + if self.http_client { + caps.http_client = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all(); + } + + caps.threading.enable_asynchronous_threading = self.enable_async_threads; + + caps + } + pub fn prepare_runtime( &self, engine: Engine, @@ -297,162 +300,6 @@ impl Wasi { Ok((wasi_env, instance)) } - // Runs the Wasi process - pub fn run(run: RunProperties, store: Store) -> Result { - let tasks = run.ctx.data(&store).tasks().clone(); - - // The return value is passed synchronously and will block until the result is returned - // this is because the main thread can go into a deep sleep and exit the dedicated thread - let (tx, rx) = std::sync::mpsc::channel(); - - // We run it in a blocking thread as the WASM function may otherwise hold - // up the IO operations - tasks.task_dedicated(Box::new(move || { - Self::run_with_deep_sleep(run, store, tx, None); - }))?; - rx.recv() - .expect("main thread terminated without a result, this normally means a panic occurred within the main thread") - } - - // Runs the Wasi process (asynchronously) - pub fn run_with_deep_sleep( - run: RunProperties, - mut store: Store, - tx: Sender>, - rewind_state: Option<(RewindState, Bytes)>, - ) { - // If we need to rewind then do so - let ctx = run.ctx; - if let Some((rewind_state, rewind_result)) = rewind_state { - if rewind_state.is_64bit { - let res = rewind_ext::( - ctx.env.clone().into_mut(&mut store), - rewind_state.memory_stack, - rewind_state.rewind_stack, - rewind_state.store_data, - rewind_result, - ); - if res != Errno::Success { - tx.send(Ok(res as i32)).ok(); - return; - } - } else { - let res = rewind_ext::( - ctx.env.clone().into_mut(&mut store), - rewind_state.memory_stack, - rewind_state.rewind_stack, - rewind_state.store_data, - rewind_result, - ); - if res != Errno::Success { - tx.send(Ok(res as i32)).ok(); - return; - } - } - } - - // Get the instance from the environment - let instance = match ctx.data(&store).try_clone_instance() { - Some(inst) => inst, - None => { - tx.send(Ok(Errno::Noexec as i32)).ok(); - return; - } - }; - - // Do we want to invoke a function? - if let Some(ref invoke) = run.invoke { - let res = RunWithPathBuf::inner_module_invoke_function( - &mut store, - &instance, - run.path.as_path(), - invoke, - &run.args, - ) - .map(|()| 0); - - ctx.cleanup(&mut store, None); - - tx.send(res).unwrap(); - } else { - let start: Function = - RunWithPathBuf::try_find_function(&instance, run.path.as_path(), "_start", &[]) - .unwrap(); - - let result = start.call(&mut store, &[]); - Self::handle_result( - RunProperties { - ctx, - path: run.path, - invoke: run.invoke, - args: run.args, - }, - store, - result, - tx, - ) - } - } - - /// Helper function for handling the result of a Wasi _start function. - pub fn handle_result( - run: RunProperties, - mut store: Store, - result: Result, RuntimeError>, - tx: Sender>, - ) { - let ctx = run.ctx; - let ret: Result = match result { - Ok(_) => Ok(0), - Err(err) => { - match err.downcast::() { - Ok(WasiError::Exit(exit_code)) => Ok(exit_code.raw()), - Ok(WasiError::DeepSleep(deep)) => { - let pid = ctx.data(&store).pid(); - let tid = ctx.data(&store).tid(); - tracing::trace!(%pid, %tid, "entered a deep sleep"); - - // Create the respawn function - let tasks = ctx.data(&store).tasks().clone(); - let rewind = deep.rewind; - let respawn = { - let path = run.path; - let invoke = run.invoke; - let args = run.args; - move |ctx, store, res| { - let run = RunProperties { - ctx, - path, - invoke, - args, - }; - Self::run_with_deep_sleep(run, store, tx, Some((rewind, res))); - } - }; - - // Spawns the WASM process after a trigger - unsafe { - tasks.resume_wasm_after_poller( - Box::new(respawn), - ctx, - store, - deep.trigger, - ) - } - .unwrap(); - return; - } - Ok(err) => Err(err.into()), - Err(err) => Err(err.into()), - } - } - }; - - ctx.cleanup(&mut store, None); - - tx.send(ret).unwrap(); - } - pub fn for_binfmt_interpreter() -> Result { use std::env; let dir = env::var_os("WASMER_BINFMT_MISC_PREOPEN") diff --git a/lib/cli/src/commands/run_unstable.rs b/lib/cli/src/commands/run_unstable.rs deleted file mode 100644 index 9ced59358f2..00000000000 --- a/lib/cli/src/commands/run_unstable.rs +++ /dev/null @@ -1,668 +0,0 @@ -#![allow(missing_docs, unused)] - -use std::{ - collections::BTreeMap, - fmt::{Binary, Display}, - fs::File, - io::{ErrorKind, LineWriter, Read, Write}, - net::SocketAddr, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, Mutex}, - time::{Duration, SystemTime}, -}; - -use anyhow::{Context, Error}; -use clap::Parser; -use clap_verbosity_flag::WarnLevel; -use once_cell::sync::Lazy; -use sha2::{Digest, Sha256}; -use tempfile::NamedTempFile; -use tokio::runtime::Handle; -use url::Url; -use wapm_targz_to_pirita::{FileMap, TransformManifestFunctions}; -use wasmer::{ - DeserializeError, Engine, Function, Imports, Instance, Module, Store, Type, TypedFunction, - Value, -}; -#[cfg(feature = "compiler")] -use wasmer_compiler::ArtifactBuild; -use wasmer_registry::Package; -use wasmer_wasix::{ - bin_factory::BinaryPackage, - runners::{MappedDirectory, Runner}, - runtime::resolver::PackageSpecifier, -}; -use wasmer_wasix::{ - runners::{ - emscripten::EmscriptenRunner, - wasi::WasiRunner, - wcgi::{AbortHandle, WcgiRunner}, - }, - Runtime, -}; -use webc::{metadata::Manifest, v1::DirOrFile, Container}; - -use crate::store::StoreOptions; - -static WASMER_HOME: Lazy = Lazy::new(|| { - wasmer_registry::WasmerConfig::get_wasmer_dir() - .ok() - .or_else(|| dirs::home_dir().map(|home| home.join(".wasmer"))) - .unwrap_or_else(|| PathBuf::from(".wasmer")) -}); - -/// The unstable `wasmer run` subcommand. -#[derive(Debug, Parser)] -pub struct RunUnstable { - #[clap(flatten)] - verbosity: clap_verbosity_flag::Verbosity, - /// The Wasmer home directory. - #[clap(long = "wasmer-dir", env = "WASMER_DIR", default_value = WASMER_HOME.as_os_str())] - wasmer_dir: PathBuf, - #[clap(flatten)] - store: StoreOptions, - #[clap(flatten)] - wasi: crate::commands::run::Wasi, - #[clap(flatten)] - wcgi: WcgiOptions, - #[cfg(feature = "sys")] - /// The stack size (default is 1048576) - #[clap(long = "stack-size")] - stack_size: Option, - /// The function or command to invoke. - #[clap(short, long, aliases = &["command", "invoke", "command-name"])] - entrypoint: Option, - /// Generate a coredump at this path if a WebAssembly trap occurs - #[clap(name = "COREDUMP PATH", long)] - coredump_on_trap: Option, - /// The file, URL, or package to run. - #[clap(value_parser = PackageSource::infer)] - input: PackageSource, - /// Command-line arguments passed to the package - args: Vec, -} - -impl RunUnstable { - pub fn execute(&self) -> Result<(), Error> { - crate::logging::set_up_logging(self.verbosity.log_level_filter()); - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()?; - let handle = runtime.handle().clone(); - - #[cfg(feature = "sys")] - if self.stack_size.is_some() { - wasmer_vm::set_stack_size(self.stack_size.unwrap()); - } - - let (mut store, _) = self.store.get_store()?; - let runtime = - self.wasi - .prepare_runtime(store.engine().clone(), &self.wasmer_dir, handle)?; - - let target = self - .input - .resolve_target(&runtime) - .with_context(|| format!("Unable to resolve \"{}\"", self.input))?; - - let result = self.execute_target(target, Arc::new(runtime), &mut store); - - if let Err(e) = &result { - self.maybe_save_coredump(e); - } - - result - } - - fn execute_target( - &self, - executable_target: ExecutableTarget, - runtime: Arc, - store: &mut Store, - ) -> Result<(), Error> { - match executable_target { - ExecutableTarget::WebAssembly { module, path } => { - self.execute_wasm(&path, &module, store, runtime) - } - ExecutableTarget::Package(pkg) => self.execute_webc(&pkg, runtime), - } - } - - #[tracing::instrument(skip_all)] - fn execute_wasm( - &self, - path: &Path, - module: &Module, - store: &mut Store, - runtime: Arc, - ) -> Result<(), Error> { - if wasmer_emscripten::is_emscripten_module(module) { - self.execute_emscripten_module() - } else if wasmer_wasix::is_wasi_module(module) || wasmer_wasix::is_wasix_module(module) { - self.execute_wasi_module(path, module, runtime, store) - } else { - self.execute_pure_wasm_module(module, store) - } - } - - #[tracing::instrument(skip_all)] - fn execute_webc( - &self, - pkg: &BinaryPackage, - runtime: Arc, - ) -> Result<(), Error> { - let id = match self.entrypoint.as_deref() { - Some(cmd) => cmd, - None => infer_webc_entrypoint(pkg)?, - }; - let cmd = pkg - .get_command(id) - .with_context(|| format!("Unable to get metadata for the \"{id}\" command"))?; - - let uses = self.load_injected_packages(&*runtime)?; - - if WcgiRunner::can_run_command(cmd.metadata())? { - self.run_wcgi(id, pkg, uses, runtime) - } else if WasiRunner::can_run_command(cmd.metadata())? { - self.run_wasi(id, pkg, uses, runtime) - } else if EmscriptenRunner::can_run_command(cmd.metadata())? { - self.run_emscripten(id, pkg, runtime) - } else { - anyhow::bail!( - "Unable to find a runner that supports \"{}\"", - cmd.metadata().runner - ); - } - } - - #[tracing::instrument(level = "debug", skip_all)] - fn load_injected_packages(&self, runtime: &dyn Runtime) -> Result, Error> { - let mut dependencies = Vec::new(); - - for name in &self.wasi.uses { - let specifier = PackageSpecifier::parse(name) - .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?; - let pkg = runtime - .task_manager() - .block_on(BinaryPackage::from_registry(&specifier, runtime)) - .with_context(|| format!("Unable to load \"{name}\""))?; - dependencies.push(pkg); - } - - Ok(dependencies) - } - - fn run_wasi( - &self, - command_name: &str, - pkg: &BinaryPackage, - uses: Vec, - runtime: Arc, - ) -> Result<(), Error> { - let mut runner = wasmer_wasix::runners::wasi::WasiRunner::new() - .with_args(self.args.clone()) - .with_envs(self.wasi.env_vars.clone()) - .with_mapped_directories(self.wasi.mapped_dirs.clone()) - .with_injected_packages(uses); - if self.wasi.forward_host_env { - runner.set_forward_host_env(); - } - - runner.run_command(command_name, pkg, runtime) - } - - fn run_wcgi( - &self, - command_name: &str, - pkg: &BinaryPackage, - uses: Vec, - runtime: Arc, - ) -> Result<(), Error> { - let mut runner = wasmer_wasix::runners::wcgi::WcgiRunner::new(); - - runner - .config() - .args(self.args.clone()) - .addr(self.wcgi.addr) - .envs(self.wasi.env_vars.clone()) - .map_directories(self.wasi.mapped_dirs.clone()) - .callbacks(Callbacks::new(self.wcgi.addr)) - .inject_packages(uses); - if self.wasi.forward_host_env { - runner.config().forward_host_env(); - } - - runner.run_command(command_name, pkg, runtime) - } - - fn run_emscripten( - &self, - command_name: &str, - pkg: &BinaryPackage, - runtime: Arc, - ) -> Result<(), Error> { - let mut runner = wasmer_wasix::runners::emscripten::EmscriptenRunner::new(); - runner.set_args(self.args.clone()); - - runner.run_command(command_name, pkg, runtime) - } - - #[tracing::instrument(skip_all)] - fn execute_pure_wasm_module(&self, module: &Module, store: &mut Store) -> Result<(), Error> { - let imports = Imports::default(); - let instance = Instance::new(store, module, &imports) - .context("Unable to instantiate the WebAssembly module")?; - - let entrypoint = match &self.entrypoint { - Some(entry) => { - instance.exports - .get_function(entry) - .with_context(|| format!("The module doesn't contain a \"{entry}\" function"))? - }, - None => { - instance.exports.get_function("_start") - .context("The module doesn't contain a \"_start\" function. Either implement it or specify an entrypoint function.")? - } - }; - - let return_values = invoke_function(&instance, store, entrypoint, &self.args)?; - - println!( - "{}", - return_values - .iter() - .map(|val| val.to_string()) - .collect::>() - .join(" ") - ); - - Ok(()) - } - - #[tracing::instrument(skip_all)] - fn execute_wasi_module( - &self, - wasm_path: &Path, - module: &Module, - runtime: Arc, - store: &mut Store, - ) -> Result<(), Error> { - let program_name = wasm_path.display().to_string(); - - let builder = self - .wasi - .prepare(module, program_name, self.args.clone(), runtime)?; - - builder.run_with_store(module.clone(), store)?; - - Ok(()) - } - - #[tracing::instrument(skip_all)] - fn execute_emscripten_module(&self) -> Result<(), Error> { - anyhow::bail!("Emscripten packages are not currently supported") - } - - #[allow(unused_variables)] - fn maybe_save_coredump(&self, e: &Error) { - #[cfg(feature = "coredump")] - if let Some(coredump) = &self.coredump_on_trap { - if let Err(e) = generate_coredump(e, self.input.to_string(), coredump) { - tracing::warn!( - error = &*e as &dyn std::error::Error, - coredump_path=%coredump.display(), - "Unable to generate a coredump", - ); - } - } - } -} - -fn invoke_function( - instance: &Instance, - store: &mut Store, - func: &Function, - args: &[String], -) -> Result, Error> { - let func_ty = func.ty(store); - let required_arguments = func_ty.params().len(); - let provided_arguments = args.len(); - - anyhow::ensure!( - required_arguments == provided_arguments, - "Function expected {} arguments, but received {}", - required_arguments, - provided_arguments, - ); - - let invoke_args = args - .iter() - .zip(func_ty.params().iter()) - .map(|(arg, param_type)| { - parse_value(arg, *param_type) - .with_context(|| format!("Unable to convert {arg:?} to {param_type:?}")) - }) - .collect::, _>>()?; - - let return_values = func.call(store, &invoke_args)?; - - Ok(return_values) -} - -fn parse_value(s: &str, ty: wasmer_types::Type) -> Result { - let value = match ty { - Type::I32 => Value::I32(s.parse()?), - Type::I64 => Value::I64(s.parse()?), - Type::F32 => Value::F32(s.parse()?), - Type::F64 => Value::F64(s.parse()?), - Type::V128 => Value::V128(s.parse()?), - _ => anyhow::bail!("There is no known conversion from {s:?} to {ty:?}"), - }; - Ok(value) -} - -fn infer_webc_entrypoint(pkg: &BinaryPackage) -> Result<&str, Error> { - if let Some(entrypoint) = pkg.entrypoint_cmd.as_deref() { - return Ok(entrypoint); - } - - match pkg.commands.as_slice() { - [] => anyhow::bail!("The WEBC file doesn't contain any executable commands"), - [one] => Ok(one.name()), - [..] => { - let mut commands: Vec<_> = pkg.commands.iter().map(|cmd| cmd.name()).collect(); - commands.sort(); - anyhow::bail!( - "Unable to determine the WEBC file's entrypoint. Please choose one of {:?}", - commands, - ); - } - } -} - -/// The input that was passed in via the command-line. -#[derive(Debug, Clone, PartialEq)] -enum PackageSource { - /// A file on disk (`*.wasm`, `*.webc`, etc.). - File(PathBuf), - /// A directory containing a `wasmer.toml` file - Dir(PathBuf), - /// A package to be downloaded (a URL, package name, etc.) - Package(PackageSpecifier), -} - -impl PackageSource { - fn infer(s: &str) -> Result { - let path = Path::new(s); - if path.is_file() { - return Ok(PackageSource::File(path.to_path_buf())); - } else if path.is_dir() { - return Ok(PackageSource::Dir(path.to_path_buf())); - } - - if let Ok(pkg) = PackageSpecifier::parse(s) { - return Ok(PackageSource::Package(pkg)); - } - - Err(anyhow::anyhow!( - "Unable to resolve \"{s}\" as a URL, package name, or file on disk" - )) - } - - /// Try to resolve the [`PackageSource`] to an executable artifact. - /// - /// This will try to automatically download and cache any resources from the - /// internet. - fn resolve_target(&self, rt: &dyn Runtime) -> Result { - match self { - PackageSource::File(path) => ExecutableTarget::from_file(path, rt), - PackageSource::Dir(d) => ExecutableTarget::from_dir(d, rt), - PackageSource::Package(pkg) => { - let pkg = rt - .task_manager() - .block_on(BinaryPackage::from_registry(pkg, rt))?; - Ok(ExecutableTarget::Package(pkg)) - } - } - } -} - -impl Display for PackageSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PackageSource::File(path) | PackageSource::Dir(path) => write!(f, "{}", path.display()), - PackageSource::Package(p) => write!(f, "{p}"), - } - } -} - -/// We've been given the path for a file... What does it contain and how should -/// that be run? -#[derive(Debug, Clone)] -enum TargetOnDisk { - WebAssemblyBinary, - Wat, - LocalWebc, - Artifact, -} - -impl TargetOnDisk { - fn from_file(path: &Path) -> Result { - // Normally the first couple hundred bytes is enough to figure - // out what type of file this is. - let mut buffer = [0_u8; 512]; - - let mut f = File::open(path) - .with_context(|| format!("Unable to open \"{}\" for reading", path.display(),))?; - let bytes_read = f.read(&mut buffer)?; - - let leading_bytes = &buffer[..bytes_read]; - - if wasmer::is_wasm(leading_bytes) { - return Ok(TargetOnDisk::WebAssemblyBinary); - } - - if webc::detect(leading_bytes).is_ok() { - return Ok(TargetOnDisk::LocalWebc); - } - - #[cfg(feature = "compiler")] - if ArtifactBuild::is_deserializable(leading_bytes) { - return Ok(TargetOnDisk::Artifact); - } - - // If we can't figure out the file type based on its content, fall back - // to checking the extension. - - match path.extension().and_then(|s| s.to_str()) { - Some("wat") => Ok(TargetOnDisk::Wat), - Some("wasm") => Ok(TargetOnDisk::WebAssemblyBinary), - Some("webc") => Ok(TargetOnDisk::LocalWebc), - Some("wasmu") => Ok(TargetOnDisk::WebAssemblyBinary), - _ => anyhow::bail!("Unable to determine how to execute \"{}\"", path.display()), - } - } -} - -#[derive(Debug, Clone)] -enum ExecutableTarget { - WebAssembly { module: Module, path: PathBuf }, - Package(BinaryPackage), -} - -impl ExecutableTarget { - /// Try to load a Wasmer package from a directory containing a `wasmer.toml` - /// file. - fn from_dir(dir: &Path, runtime: &dyn Runtime) -> Result { - let webc = construct_webc_in_memory(dir)?; - let container = Container::from_bytes(webc)?; - - let pkg = runtime - .task_manager() - .block_on(BinaryPackage::from_webc(&container, runtime))?; - - Ok(ExecutableTarget::Package(pkg)) - } - - /// Try to load a file into something that can be used to run it. - #[tracing::instrument(skip_all)] - fn from_file(path: &Path, runtime: &dyn Runtime) -> Result { - match TargetOnDisk::from_file(path)? { - TargetOnDisk::WebAssemblyBinary | TargetOnDisk::Wat => { - let wasm = std::fs::read(path)?; - let engine = runtime.engine().context("No engine available")?; - let module = Module::new(&engine, wasm)?; - Ok(ExecutableTarget::WebAssembly { - module, - path: path.to_path_buf(), - }) - } - TargetOnDisk::Artifact => { - let engine = runtime.engine().context("No engine available")?; - let module = unsafe { Module::deserialize_from_file(&engine, path)? }; - - Ok(ExecutableTarget::WebAssembly { - module, - path: path.to_path_buf(), - }) - } - TargetOnDisk::LocalWebc => { - let container = Container::from_disk(path)?; - let pkg = runtime - .task_manager() - .block_on(BinaryPackage::from_webc(&container, runtime))?; - Ok(ExecutableTarget::Package(pkg)) - } - } - } -} - -#[tracing::instrument(level = "debug", skip_all)] -fn construct_webc_in_memory(dir: &Path) -> Result, Error> { - let mut files = BTreeMap::new(); - load_files_from_disk(&mut files, dir, dir)?; - - let wasmer_toml = DirOrFile::File("wasmer.toml".into()); - if let Some(toml_data) = files.remove(&wasmer_toml) { - // HACK(Michael-F-Bryan): The version of wapm-targz-to-pirita we are - // using doesn't know we renamed "wapm.toml" to "wasmer.toml", so we - // manually patch things up if people have already migrated their - // projects. - files - .entry(DirOrFile::File("wapm.toml".into())) - .or_insert(toml_data); - } - - let functions = TransformManifestFunctions::default(); - let webc = wapm_targz_to_pirita::generate_webc_file(files, dir, None, &functions)?; - - Ok(webc) -} - -fn load_files_from_disk(files: &mut FileMap, dir: &Path, base: &Path) -> Result<(), Error> { - let entries = dir - .read_dir() - .with_context(|| format!("Unable to read the contents of \"{}\"", dir.display()))?; - - for entry in entries { - let path = entry?.path(); - let relative_path = path.strip_prefix(base)?.to_path_buf(); - - if path.is_dir() { - load_files_from_disk(files, &path, base)?; - files.insert(DirOrFile::Dir(relative_path), Vec::new()); - } else if path.is_file() { - let data = std::fs::read(&path) - .with_context(|| format!("Unable to read \"{}\"", path.display()))?; - files.insert(DirOrFile::File(relative_path), data); - } - } - Ok(()) -} - -#[cfg(feature = "coredump")] -fn generate_coredump(err: &Error, source_name: String, coredump_path: &Path) -> Result<(), Error> { - let err: &wasmer::RuntimeError = match err.downcast_ref() { - Some(e) => e, - None => { - log::warn!("no runtime error found to generate coredump with"); - return Ok(()); - } - }; - - let mut coredump_builder = - wasm_coredump_builder::CoredumpBuilder::new().executable_name(&source_name); - - let mut thread_builder = wasm_coredump_builder::ThreadBuilder::new().thread_name("main"); - - for frame in err.trace() { - let coredump_frame = wasm_coredump_builder::FrameBuilder::new() - .codeoffset(frame.func_offset() as u32) - .funcidx(frame.func_index()) - .build(); - thread_builder.add_frame(coredump_frame); - } - - coredump_builder.add_thread(thread_builder.build()); - - let coredump = coredump_builder - .serialize() - .map_err(Error::msg) - .context("Coredump serializing failed")?; - - std::fs::write(coredump_path, &coredump).with_context(|| { - format!( - "Unable to save the coredump to \"{}\"", - coredump_path.display() - ) - })?; - - Ok(()) -} - -#[derive(Debug, Clone, Parser)] -pub(crate) struct WcgiOptions { - /// The address to serve on. - #[clap(long, short, env, default_value_t = ([127, 0, 0, 1], 8000).into())] - pub(crate) addr: SocketAddr, -} - -impl Default for WcgiOptions { - fn default() -> Self { - Self { - addr: ([127, 0, 0, 1], 8000).into(), - } - } -} - -#[derive(Debug)] -struct Callbacks { - stderr: Mutex>, - addr: SocketAddr, -} - -impl Callbacks { - fn new(addr: SocketAddr) -> Self { - Callbacks { - stderr: Mutex::new(LineWriter::new(std::io::stderr())), - addr, - } - } -} - -impl wasmer_wasix::runners::wcgi::Callbacks for Callbacks { - fn started(&self, _abort: AbortHandle) { - println!("WCGI Server running at http://{}/", self.addr); - } - - fn on_stderr(&self, raw_message: &[u8]) { - if let Ok(mut stderr) = self.stderr.lock() { - // If the WCGI runner printed any log messages we want to make sure - // they get propagated to the user. Line buffering is important here - // because it helps prevent the output from becoming a complete - // mess. - let _ = stderr.write_all(raw_message); - } - } -} diff --git a/lib/cli/src/error.rs b/lib/cli/src/error.rs index d05b57850ef..8a2970e7dad 100644 --- a/lib/cli/src/error.rs +++ b/lib/cli/src/error.rs @@ -10,6 +10,13 @@ pub struct PrettyError { error: Error, } +impl PrettyError { + /// Create a new [`PrettyError`]. + pub fn new(error: Error) -> Self { + PrettyError { error } + } +} + /// A macro that prints a warning with nice colors #[macro_export] macro_rules! warning { diff --git a/lib/virtual-fs/src/mem_fs/file_opener.rs b/lib/virtual-fs/src/mem_fs/file_opener.rs index 525ab0178d4..00e27cf621f 100644 --- a/lib/virtual-fs/src/mem_fs/file_opener.rs +++ b/lib/virtual-fs/src/mem_fs/file_opener.rs @@ -69,14 +69,15 @@ impl FileSystem { /// Inserts a arc file into the file system that references another file /// in another file system (does not copy the real data) - pub fn insert_arc_file( + pub fn insert_arc_file_at( &self, - path: PathBuf, + target_path: PathBuf, fs: Arc, + source_path: PathBuf, ) -> Result<()> { - let _ = crate::FileSystem::remove_file(self, path.as_path()); + let _ = crate::FileSystem::remove_file(self, target_path.as_path()); let (inode_of_parent, maybe_inode_of_file, name_of_file) = - self.insert_inode(path.as_path())?; + self.insert_inode(target_path.as_path())?; let inode_of_parent = match inode_of_parent { InodeResolution::Found(a) => a, @@ -95,7 +96,7 @@ impl FileSystem { let mut fs_lock = self.inner.write().map_err(|_| FsError::Lock)?; // Read the metadata or generate a dummy one - let meta = match fs.metadata(&path) { + let meta = match fs.metadata(&target_path) { Ok(meta) => meta, _ => { let time = time(); @@ -118,7 +119,7 @@ impl FileSystem { inode: inode_of_file, name: name_of_file, fs, - path, + path: source_path, metadata: meta, })); @@ -136,16 +137,27 @@ impl FileSystem { Ok(()) } - /// Inserts a arc directory into the file system that references another file + /// Inserts a arc file into the file system that references another file /// in another file system (does not copy the real data) - pub fn insert_arc_directory( + pub fn insert_arc_file( &self, - path: PathBuf, + target_path: PathBuf, fs: Arc, ) -> Result<()> { - let _ = crate::FileSystem::remove_dir(self, path.as_path()); + self.insert_arc_file_at(target_path.clone(), fs, target_path) + } + + /// Inserts a arc directory into the file system that references another file + /// in another file system (does not copy the real data) + pub fn insert_arc_directory_at( + &self, + target_path: PathBuf, + other: Arc, + source_path: PathBuf, + ) -> Result<()> { + let _ = crate::FileSystem::remove_dir(self, target_path.as_path()); let (inode_of_parent, maybe_inode_of_file, name_of_file) = - self.insert_inode(path.as_path())?; + self.insert_inode(target_path.as_path())?; let inode_of_parent = match inode_of_parent { InodeResolution::Found(a) => a, @@ -169,8 +181,8 @@ impl FileSystem { fs_lock.storage.insert(Node::ArcDirectory(ArcDirectoryNode { inode: inode_of_file, name: name_of_file, - fs, - path, + fs: other, + path: source_path, metadata: { let time = time(); Metadata { @@ -200,6 +212,16 @@ impl FileSystem { Ok(()) } + /// Inserts a arc directory into the file system that references another file + /// in another file system (does not copy the real data) + pub fn insert_arc_directory( + &self, + target_path: PathBuf, + other: Arc, + ) -> Result<()> { + self.insert_arc_directory_at(target_path.clone(), other, target_path) + } + /// Inserts a arc file into the file system that references another file /// in another file system (does not copy the real data) pub fn insert_device_file( diff --git a/lib/virtual-fs/src/mem_fs/filesystem.rs b/lib/virtual-fs/src/mem_fs/filesystem.rs index 356580b7b40..d5a13920030 100644 --- a/lib/virtual-fs/src/mem_fs/filesystem.rs +++ b/lib/virtual-fs/src/mem_fs/filesystem.rs @@ -1,7 +1,7 @@ //! This module contains the [`FileSystem`] type itself. use super::*; -use crate::{DirEntry, FileType, FsError, Metadata, OpenOptions, ReadDir, Result}; +use crate::{DirEntry, FileSystem as _, FileType, FsError, Metadata, OpenOptions, ReadDir, Result}; use slab::Slab; use std::collections::VecDeque; use std::convert::identity; @@ -28,6 +28,79 @@ impl FileSystem { self } + /// Canonicalize a path without validating that it actually exists. + pub fn canonicalize_unchecked(&self, path: &Path) -> Result { + let lock = self.inner.read().map_err(|_| FsError::Lock)?; + lock.canonicalize_without_inode(path) + } + + /// Merge all items from a given source path (directory) of a different file + /// system into this file system. + /// + /// Individual files and directories of the given path are mounted. + /// + /// This function is not recursive, only the items in the source_path are + /// mounted. + /// + /// See [`Self::union`] for mounting all inodes recursively. + pub fn mount_directory_entries( + &self, + target_path: &Path, + other: &Arc, + mut source_path: &Path, + ) -> Result<()> { + let fs_lock = self.inner.read().map_err(|_| FsError::Lock)?; + + if cfg!(windows) { + // We need to take some care here because + // canonicalize_without_inode() doesn't accept Windows paths that + // start with a prefix (drive letters, UNC paths, etc.). If we + // somehow get one of those paths, we'll automatically trim it away. + let mut components = source_path.components(); + + if let Some(Component::Prefix(_)) = components.next() { + source_path = components.as_path(); + } + } + + let (_target_path, root_inode) = match fs_lock.canonicalize(target_path) { + Ok((p, InodeResolution::Found(inode))) => (p, inode), + Ok((_p, InodeResolution::Redirect(..))) => { + return Err(FsError::AlreadyExists); + } + Err(_) => { + // Root directory does not exist, so we can just mount. + return self.mount(target_path.to_path_buf(), other, source_path.to_path_buf()); + } + }; + + let _root_node = match fs_lock.storage.get(root_inode).unwrap() { + Node::Directory(dir) => dir, + _ => { + return Err(FsError::AlreadyExists); + } + }; + + let source_path = fs_lock.canonicalize_without_inode(source_path)?; + + std::mem::drop(fs_lock); + + let source = other.read_dir(&source_path)?; + for entry in source.data { + let meta = entry.metadata?; + + let entry_target_path = target_path.join(entry.path.file_name().unwrap()); + + if meta.is_file() { + self.insert_arc_file_at(entry_target_path, other.clone(), entry.path)?; + } else if meta.is_dir() { + self.insert_arc_directory_at(entry_target_path, other.clone(), entry.path)?; + } + } + + Ok(()) + } + pub fn union(&self, other: &Arc) { // Iterate all the directories and files in the other filesystem // and create references back to them in this filesystem @@ -90,11 +163,11 @@ impl FileSystem { pub fn mount( &self, - path: PathBuf, + target_path: PathBuf, other: &Arc, - dst: PathBuf, + source_path: PathBuf, ) -> Result<()> { - if crate::FileSystem::read_dir(self, path.as_path()).is_ok() { + if crate::FileSystem::read_dir(self, target_path.as_path()).is_ok() { return Err(FsError::AlreadyExists); } @@ -104,7 +177,7 @@ impl FileSystem { // Canonicalize the path without checking the path exists, // because it's about to be created. - let path = guard.canonicalize_without_inode(path.as_path())?; + let path = guard.canonicalize_without_inode(target_path.as_path())?; // Check the path has a parent. let parent_of_path = path.parent().ok_or(FsError::BaseNotDirectory)?; @@ -136,7 +209,7 @@ impl FileSystem { inode: inode_of_directory, name: name_of_directory, fs: other.clone(), - path: dst, + path: source_path, metadata: { let time = time(); @@ -962,7 +1035,11 @@ impl DirectoryMustBeEmpty { #[cfg(test)] mod test_filesystem { - use crate::{mem_fs::*, ops, DirEntry, FileSystem as FS, FileType, FsError}; + use std::{borrow::Cow, path::Path}; + + use tokio::io::AsyncReadExt; + + use crate::{mem_fs::*, ops, DirEntry, FileOpener, FileSystem as FS, FileType, FsError}; macro_rules! path { ($path:expr) => { @@ -1731,4 +1808,40 @@ mod test_filesystem { assert!(ops::is_dir(&fs, "/top-level/nested")); assert!(ops::is_file(&fs, "/top-level/nested/another-file.txt")); } + + #[tokio::test] + async fn test_merge_flat() { + let main = FileSystem::default(); + + let other = FileSystem::default(); + crate::ops::create_dir_all(&other, "/a/x").unwrap(); + other + .insert_ro_file(&Path::new("/a/x/a.txt"), Cow::Borrowed(b"a")) + .unwrap(); + other + .insert_ro_file(&Path::new("/a/x/b.txt"), Cow::Borrowed(b"b")) + .unwrap(); + other + .insert_ro_file(&Path::new("/a/x/c.txt"), Cow::Borrowed(b"c")) + .unwrap(); + + let out = other.read_dir(&Path::new("/")).unwrap(); + dbg!(&out); + + let other: Arc = Arc::new(other); + + main.mount_directory_entries(&Path::new("/"), &other, &Path::new("/a")) + .unwrap(); + + let mut buf = Vec::new(); + + let mut f = main + .new_open_options() + .read(true) + .open(&Path::new("/x/a.txt")) + .unwrap(); + f.read_to_end(&mut buf).await.unwrap(); + + assert_eq!(buf, b"a"); + } } diff --git a/lib/virtual-fs/src/tmp_fs.rs b/lib/virtual-fs/src/tmp_fs.rs index ed1f4be0149..e72c8d33b9d 100644 --- a/lib/virtual-fs/src/tmp_fs.rs +++ b/lib/virtual-fs/src/tmp_fs.rs @@ -42,6 +42,17 @@ impl TmpFileSystem { self.fs.union(other) } + /// See [`mem_fs::FileSystem::mount_directory_entries`]. + pub fn mount_directory_entries( + &self, + target_path: &Path, + other: &Arc, + source_path: &Path, + ) -> Result<()> { + self.fs + .mount_directory_entries(target_path, other, source_path) + } + pub fn mount( &self, src_path: PathBuf, @@ -50,6 +61,11 @@ impl TmpFileSystem { ) -> Result<()> { self.fs.mount(src_path, other, dst_path) } + + /// Canonicalize a path without validating that it actually exists. + pub fn canonicalize_unchecked(&self, path: &Path) -> Result { + self.fs.canonicalize_unchecked(path) + } } impl FileSystem for TmpFileSystem { diff --git a/lib/wasi/src/capabilities.rs b/lib/wasi/src/capabilities.rs index 1468184e080..35a6eab8ee3 100644 --- a/lib/wasi/src/capabilities.rs +++ b/lib/wasi/src/capabilities.rs @@ -16,6 +16,19 @@ impl Capabilities { threading: Default::default(), } } + + /// Merges another [`Capabilities`] object into this one, overwriting fields + /// if necessary. + pub fn update(&mut self, other: Capabilities) { + let Capabilities { + insecure_allow_all, + http_client, + threading, + } = other; + self.insecure_allow_all |= insecure_allow_all; + self.http_client.update(http_client); + self.threading.update(threading); + } } impl Default for Capabilities { @@ -36,3 +49,14 @@ pub struct CapabilityThreadingV1 { /// (default = false) pub enable_asynchronous_threading: bool, } + +impl CapabilityThreadingV1 { + pub fn update(&mut self, other: CapabilityThreadingV1) { + let CapabilityThreadingV1 { + max_threads, + enable_asynchronous_threading, + } = other; + self.enable_asynchronous_threading |= enable_asynchronous_threading; + self.max_threads = max_threads.or(self.max_threads); + } +} diff --git a/lib/wasi/src/http/client.rs b/lib/wasi/src/http/client.rs index 4c576cd76c7..174709cf8ab 100644 --- a/lib/wasi/src/http/client.rs +++ b/lib/wasi/src/http/client.rs @@ -33,6 +33,15 @@ impl HttpClientCapabilityV1 { pub fn can_access_domain(&self, domain: &str) -> bool { self.allow_all || self.allowed_hosts.contains(domain) } + + pub fn update(&mut self, other: HttpClientCapabilityV1) { + let HttpClientCapabilityV1 { + allow_all, + allowed_hosts, + } = other; + self.allow_all |= allow_all; + self.allowed_hosts.extend(allowed_hosts); + } } impl Default for HttpClientCapabilityV1 { diff --git a/lib/wasi/src/runners/wasi.rs b/lib/wasi/src/runners/wasi.rs index 97eec1a8cd0..c269548d34c 100644 --- a/lib/wasi/src/runners/wasi.rs +++ b/lib/wasi/src/runners/wasi.rs @@ -7,6 +7,7 @@ use webc::metadata::{annotations::Wasi, Command}; use crate::{ bin_factory::BinaryPackage, + capabilities::Capabilities, runners::{wasi_common::CommonWasiOptions, MappedDirectory}, Runtime, WasiEnvBuilder, }; @@ -128,6 +129,10 @@ impl WasiRunner { self } + pub fn capabilities(&mut self) -> &mut Capabilities { + &mut self.wasi.capabilities + } + fn prepare_webc_env( &self, program_name: &str, @@ -170,10 +175,11 @@ impl crate::runners::Runner for WasiRunner { .unwrap_or_else(|| Wasi::new(command_name)); let module = crate::runners::compile_module(cmd.atom(), &*runtime)?; - let mut store = runtime.new_store(); + let store = runtime.new_store(); - self.prepare_webc_env(command_name, &wasi, pkg, runtime)? - .run_with_store(module, &mut store)?; + self.prepare_webc_env(command_name, &wasi, pkg, runtime) + .context("Unable to prepare the WASI environment")? + .run_with_store_async(module, store)?; Ok(()) } diff --git a/lib/wasi/src/runners/wasi_common.rs b/lib/wasi/src/runners/wasi_common.rs index 78d334e99b4..5498de74f35 100644 --- a/lib/wasi/src/runners/wasi_common.rs +++ b/lib/wasi/src/runners/wasi_common.rs @@ -8,7 +8,10 @@ use anyhow::{Context, Error}; use virtual_fs::{FileSystem, FsError, OverlayFileSystem, RootFileSystemBuilder}; use webc::metadata::annotations::Wasi as WasiAnnotation; -use crate::{bin_factory::BinaryPackage, runners::MappedDirectory, WasiEnvBuilder}; +use crate::{ + bin_factory::BinaryPackage, capabilities::Capabilities, runners::MappedDirectory, + WasiEnvBuilder, +}; #[derive(Debug, Default, Clone)] pub(crate) struct CommonWasiOptions { @@ -17,6 +20,7 @@ pub(crate) struct CommonWasiOptions { pub(crate) forward_host_env: bool, pub(crate) mapped_dirs: Vec, pub(crate) injected_packages: Vec, + pub(crate) capabilities: Capabilities, } impl CommonWasiOptions { @@ -44,6 +48,8 @@ impl CommonWasiOptions { self.populate_env(wasi, builder); self.populate_args(wasi, builder); + *builder.capabilities_mut() = self.capabilities.clone(); + Ok(()) } @@ -89,37 +95,61 @@ fn prepare_filesystem( let host_fs: Arc = Arc::new(crate::default_fs_backing()); for dir in mapped_dirs { - let MappedDirectory { host, guest } = dir; - let mut guest = PathBuf::from(guest); + let MappedDirectory { + host: host_path, + guest: guest_path, + } = dir; + let mut guest_path = PathBuf::from(guest_path); tracing::debug!( - guest=%guest.display(), - host=%host.display(), + guest=%guest_path.display(), + host=%host_path.display(), "Mounting host folder", ); - if guest.is_relative() { - guest = apply_relative_path_mounting_hack(&guest); + if guest_path.is_relative() { + guest_path = apply_relative_path_mounting_hack(&guest_path); } - if let Some(parent) = guest.parent() { - create_dir_all(&root_fs, parent).with_context(|| { - format!("Unable to create the \"{}\" directory", parent.display()) - })?; - } + let host_path = std::fs::canonicalize(host_path).with_context(|| { + format!("Unable to canonicalize host path '{}'", host_path.display()) + })?; - root_fs - .mount(guest.clone(), &host_fs, host.clone()) + let guest_path = root_fs + .canonicalize_unchecked(&guest_path) .with_context(|| { format!( - "Unable to mount \"{}\" to \"{}\"", - host.display(), - guest.display() + "Unable to canonicalize guest path '{}'", + guest_path.display() ) })?; - builder - .add_preopen_dir(&guest) - .with_context(|| format!("Unable to preopen \"{}\"", guest.display()))?; + if guest_path == Path::new("/") { + root_fs + .mount_directory_entries(&guest_path, &host_fs, &host_path) + .with_context(|| { + format!("Unable to mount \"{}\" to root", host_path.display(),) + })?; + } else { + if let Some(parent) = guest_path.parent() { + create_dir_all(&root_fs, parent).with_context(|| { + format!("Unable to create the \"{}\" directory", parent.display()) + })?; + } + + root_fs + .mount(guest_path.clone(), &host_fs, host_path.clone()) + .with_context(|| { + format!( + "Unable to mount \"{}\" to \"{}\"", + host_path.display(), + guest_path.display() + ) + })?; + + builder + .add_preopen_dir(&guest_path) + .with_context(|| format!("Unable to preopen \"{}\"", guest_path.display()))?; + } } } @@ -150,7 +180,12 @@ fn prepare_filesystem( fn apply_relative_path_mounting_hack(original: &Path) -> PathBuf { debug_assert!(original.is_relative()); - let mapped_path = Path::new("/").join(original); + let root = Path::new("/"); + let mapped_path = if original == Path::new(".") { + root.to_path_buf() + } else { + root.join(original) + }; tracing::debug!( original_path=%original.display(), diff --git a/lib/wasi/src/runners/wcgi/handler.rs b/lib/wasi/src/runners/wcgi/handler.rs index 26f372a5645..b8f5ec4720c 100644 --- a/lib/wasi/src/runners/wcgi/handler.rs +++ b/lib/wasi/src/runners/wcgi/handler.rs @@ -69,11 +69,11 @@ impl Handler { ); let task_manager = self.runtime.task_manager(); - let mut store = self.runtime.new_store(); + let store = self.runtime.new_store(); let done = task_manager .runtime() - .spawn_blocking(move || builder.run_with_store(module, &mut store)) + .spawn_blocking(move || builder.run_with_store_async(module, store)) .map_err(Error::from) .and_then(|r| async { r.map_err(Error::from) }); diff --git a/lib/wasi/src/runners/wcgi/runner.rs b/lib/wasi/src/runners/wcgi/runner.rs index cbcbfb74769..9e870e3b61b 100644 --- a/lib/wasi/src/runners/wcgi/runner.rs +++ b/lib/wasi/src/runners/wcgi/runner.rs @@ -15,6 +15,7 @@ use webc::metadata::{ use crate::{ bin_factory::BinaryPackage, + capabilities::Capabilities, runners::{ wasi_common::CommonWasiOptions, wcgi::handler::{Handler, SharedState}, @@ -235,6 +236,10 @@ impl Config { self.wasi.injected_packages.extend(packages); self } + + pub fn capabilities(&mut self) -> &mut Capabilities { + &mut self.wasi.capabilities + } } impl Default for Config { diff --git a/lib/wasi/src/state/builder.rs b/lib/wasi/src/state/builder.rs index 7fbb0dc223f..dab216904ac 100644 --- a/lib/wasi/src/state/builder.rs +++ b/lib/wasi/src/state/builder.rs @@ -6,11 +6,12 @@ use std::{ sync::Arc, }; +use bytes::Bytes; use rand::Rng; use thiserror::Error; use virtual_fs::{ArcFile, FsError, TmpFileSystem, VirtualFile}; -use wasmer::{AsStoreMut, Instance, Module, Store}; -use wasmer_wasix_types::wasi::Errno; +use wasmer::{AsStoreMut, Instance, Module, RuntimeError, Store}; +use wasmer_wasix_types::wasi::{Errno, ExitCode}; #[cfg(feature = "sys")] use crate::PluggableRuntime; @@ -21,7 +22,7 @@ use crate::{ os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane}, state::WasiState, syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO}, - Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, + RewindState, Runtime, WasiEnv, WasiError, WasiFunctionEnv, WasiRuntimeError, }; use super::env::WasiEnvInit; @@ -792,40 +793,197 @@ impl WasiEnvBuilder { #[allow(clippy::result_large_err)] pub fn run_with_store(self, module: Module, store: &mut Store) -> Result<(), WasiRuntimeError> { + if self.capabilites.threading.enable_asynchronous_threading { + tracing::warn!( + "The enable_asynchronous_threading capability is enabled. Use WasiEnvBuilder::run_with_store_async() to avoid spurious errors.", + ); + } + let (instance, env) = self.instantiate(module, store)?; let start = instance.exports.get_function("_start")?; + env.data(&store).thread.set_status_running(); - env.data(store).thread.set_status_running(); - let mut res = crate::run_wasi_func_start(start, store); + let result = crate::run_wasi_func_start(start, store); + let (result, exit_code) = wasi_exit_code(result); + let pid = env.data(&store).pid(); + let tid = env.data(&store).tid(); tracing::trace!( - "wasi[{}:{}]::main exit (code = {:?})", - env.data(store).pid(), - env.data(store).tid(), - res + %pid, + %tid, + %exit_code, + error=result.as_ref().err().map(|e| e as &dyn std::error::Error), + "main exit", ); - let exit_code = match &res { - Ok(_) => Errno::Success.into(), - Err(err) => match err.as_exit_code() { - Some(code) if code.is_success() => { - // This is actually not an error, so we need to fix up the - // result - res = Ok(()); - Errno::Success.into() - } - Some(other) => other, - None => Errno::Noexec.into(), - }, - }; - env.cleanup(store, Some(exit_code)); - res + result + } + + /// Start the WASI executable with async threads enabled. + #[allow(clippy::result_large_err)] + pub fn run_with_store_async( + self, + module: Module, + mut store: Store, + ) -> Result<(), WasiRuntimeError> { + let (_, env) = self.instantiate(module, &mut store)?; + + env.data(&store).thread.set_status_running(); + + let tasks = env.data(&store).tasks().clone(); + let pid = env.data(&store).pid(); + let tid = env.data(&store).tid(); + + // The return value is passed synchronously and will block until the result + // is returned this is because the main thread can go into a deep sleep and + // exit the dedicated thread + let (tx, rx) = std::sync::mpsc::channel(); + + tasks.task_dedicated(Box::new(move || { + run_with_deep_sleep(store, None, env, tx); + }))?; + + let result = rx.recv().expect( + "main thread terminated without a result, this normally means a panic occurred", + ); + let (result, exit_code) = wasi_exit_code(result); + + tracing::trace!( + %pid, + %tid, + %exit_code, + error=result.as_ref().err().map(|e| e as &dyn std::error::Error), + "main exit", + ); + + result } } +/// Extract the exit code from a `Result<(), WasiRuntimeError>`. +/// +/// We need this because calling `exit(0)` inside a WASI program technically +/// triggers [`WasiError`] with an exit code of `0`, but the end user won't want +/// that treated as an error. +fn wasi_exit_code( + mut result: Result<(), WasiRuntimeError>, +) -> (Result<(), WasiRuntimeError>, ExitCode) { + let exit_code = match &result { + Ok(_) => Errno::Success.into(), + Err(err) => match err.as_exit_code() { + Some(code) if code.is_success() => { + // This is actually not an error, so we need to fix up the + // result + result = Ok(()); + Errno::Success.into() + } + Some(other) => other, + None => Errno::Noexec.into(), + }, + }; + + (result, exit_code) +} + +fn run_with_deep_sleep( + mut store: Store, + rewind_state: Option<(RewindState, Bytes)>, + env: WasiFunctionEnv, + sender: std::sync::mpsc::Sender>, +) { + if let Some((rewind_state, rewind_result)) = rewind_state { + tracing::trace!("Rewinding"); + let errno = if rewind_state.is_64bit { + crate::rewind_ext::( + env.env.clone().into_mut(&mut store), + rewind_state.memory_stack, + rewind_state.rewind_stack, + rewind_state.store_data, + rewind_result, + ) + } else { + crate::rewind_ext::( + env.env.clone().into_mut(&mut store), + rewind_state.memory_stack, + rewind_state.rewind_stack, + rewind_state.store_data, + rewind_result, + ) + }; + + if errno != Errno::Success { + let exit_code = ExitCode::from(errno); + env.cleanup(&mut store, Some(exit_code)); + let _ = sender.send(Err(WasiRuntimeError::Wasi(WasiError::Exit(exit_code)))); + return; + } + } + + let instance = match env.data(&store).try_clone_instance() { + Some(instance) => instance, + None => { + tracing::debug!("Unable to clone the instance"); + env.cleanup(&mut store, None); + let _ = sender.send(Err(WasiRuntimeError::Wasi(WasiError::Exit( + Errno::Noexec.into(), + )))); + return; + } + }; + + let start = match instance.exports.get_function("_start") { + Ok(start) => start, + Err(e) => { + tracing::debug!("Unable to get the _start function"); + env.cleanup(&mut store, None); + let _ = sender.send(Err(e.into())); + return; + } + }; + + let result = start.call(&mut store, &[]); + handle_result(store, env, result, sender); +} + +fn handle_result( + mut store: Store, + env: WasiFunctionEnv, + result: Result, RuntimeError>, + sender: std::sync::mpsc::Sender>, +) { + let result = match result.map_err(|e| e.downcast::()) { + Err(Ok(WasiError::DeepSleep(work))) => { + let pid = env.data(&store).pid(); + let tid = env.data(&store).tid(); + tracing::trace!(%pid, %tid, "entered a deep sleep"); + + let tasks = env.data(&store).tasks().clone(); + let rewind = work.rewind; + let respawn = + move |ctx, store, res| run_with_deep_sleep(store, Some((rewind, res)), ctx, sender); + + // Spawns the WASM process after a trigger + unsafe { + tasks + .resume_wasm_after_poller(Box::new(respawn), env, store, work.trigger) + .unwrap(); + } + + return; + } + Ok(_) => Ok(()), + Err(Ok(other)) => Err(other.into()), + Err(Err(e)) => Err(e.into()), + }; + + let (result, exit_code) = wasi_exit_code(result); + env.cleanup(&mut store, Some(exit_code)); + let _ = sender.send(result); +} + /// Builder for preopened directories. #[derive(Debug, Default)] pub struct PreopenDirBuilder { diff --git a/tests/integration/cli/Cargo.toml b/tests/integration/cli/Cargo.toml index b8ddc21e609..33f21c322f6 100644 --- a/tests/integration/cli/Cargo.toml +++ b/tests/integration/cli/Cargo.toml @@ -22,6 +22,7 @@ assert_cmd = "2.0.8" predicates = "2.1.5" once_cell = "1.17.1" futures = "0.3.28" +regex = "1.8.3" [dependencies] anyhow = "1" diff --git a/tests/integration/cli/tests/run.rs b/tests/integration/cli/tests/run.rs index 102d3cde405..8e660a64584 100644 --- a/tests/integration/cli/tests/run.rs +++ b/tests/integration/cli/tests/run.rs @@ -1,8 +1,8 @@ //! Basic tests for the `run` subcommand -use anyhow::{bail, Context}; +use assert_cmd::Command; +use predicates::str::contains; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; use wasmer_integration_tests_cli::{get_wasmer_path, ASSET_PATH, C_ASSET_PATH}; fn wasi_test_python_path() -> PathBuf { @@ -28,63 +28,46 @@ fn test_no_start_wat_path() -> PathBuf { /// https://github.com/wasmerio/wasmer/issues/3535 // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 #[ignore] -#[cfg_attr(target_os = "windows", ignore)] #[test] -fn test_run_customlambda() -> anyhow::Result<()> { - let bindir = String::from_utf8( - Command::new(get_wasmer_path()) - .arg("config") - .arg("--bindir") - .output() - .expect("failed to run wasmer config --bindir") - .stdout, - ) - .expect("wasmer config --bindir stdout failed"); +fn test_run_customlambda() { + let assert = Command::new(get_wasmer_path()) + .arg("config") + .arg("--bindir") + .assert() + .success(); + let bindir = std::str::from_utf8(&assert.get_output().stdout) + .expect("wasmer config --bindir stdout failed"); // /Users/fs/.wasmer/bin - let checkouts_path = Path::new(&bindir) + let checkouts_path = Path::new(bindir.trim()) .parent() .expect("--bindir: no parent") .join("checkouts"); println!("checkouts path: {}", checkouts_path.display()); let _ = std::fs::remove_dir_all(&checkouts_path); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("https://wapm.io/ciuser/customlambda") // TODO: this argument should not be necessary later // see https://github.com/wasmerio/wasmer/issues/3514 .arg("customlambda.py") .arg("55") - .output()?; - - let stdout_output = std::str::from_utf8(&output.stdout).unwrap(); - let stderr_output = std::str::from_utf8(&output.stderr).unwrap(); - - println!("first run:"); - println!("stdout: {stdout_output}"); - println!("stderr: {stderr_output}"); - assert_eq!(stdout_output, "139583862445\n"); + .assert() + .success(); + assert.stdout("139583862445\n"); // Run again to verify the caching - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("https://wapm.io/ciuser/customlambda") // TODO: this argument should not be necessary later // see https://github.com/wasmerio/wasmer/issues/3514 .arg("customlambda.py") .arg("55") - .output()?; - - let stdout_output = std::str::from_utf8(&output.stdout).unwrap(); - let stderr_output = std::str::from_utf8(&output.stderr).unwrap(); - - println!("second run:"); - println!("stdout: {stdout_output}"); - println!("stderr: {stderr_output}"); - assert_eq!(stdout_output, "139583862445\n"); - - Ok(()) + .assert() + .success(); + assert.stdout("139583862445\n"); } #[allow(dead_code)] @@ -113,8 +96,8 @@ fn assert_tarball_is_present_local(target: &str) -> Result anyhow::Result<()> { - let temp_dir = tempfile::TempDir::new()?; +fn test_cross_compile_python_windows() { + let temp_dir = tempfile::TempDir::new().unwrap(); #[cfg(not(windows))] let targets = &[ @@ -158,163 +141,107 @@ fn test_cross_compile_python_windows() -> anyhow::Result<()> { let python_wasmer_path = temp_dir.path().join(format!("{t}-python")); let tarball = match std::env::var("GITHUB_TOKEN") { - Ok(_) => Some(assert_tarball_is_present_local(t)?), + Ok(_) => Some(assert_tarball_is_present_local(t).unwrap()), Err(_) => None, }; - let mut output = Command::new(get_wasmer_path()); - - output.arg("create-exe"); - output.arg(wasi_test_python_path()); - output.arg("--target"); - output.arg(t); - output.arg("-o"); - output.arg(python_wasmer_path.clone()); - output.arg(format!("--{c}")); + let mut cmd = Command::new(get_wasmer_path()); + + cmd.arg("create-exe"); + cmd.arg(wasi_test_python_path()); + cmd.arg("--target"); + cmd.arg(t); + cmd.arg("-o"); + cmd.arg(python_wasmer_path.clone()); + cmd.arg(format!("--{c}")); if std::env::var("GITHUB_TOKEN").is_ok() { - output.arg("--debug-dir"); - output.arg(format!("{t}-{c}")); + cmd.arg("--debug-dir"); + cmd.arg(format!("{t}-{c}")); } if t.contains("x86_64") && *c == "singlepass" { - output.arg("-m"); - output.arg("avx"); + cmd.arg("-m"); + cmd.arg("avx"); } if let Some(t) = tarball { - output.arg("--tarball"); - output.arg(t); + cmd.arg("--tarball"); + cmd.arg(t); } - println!("command {:?}", output); - - let output = output.output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - let stderr = std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes"); - - if !output.status.success() { - bail!("linking failed with: stdout: {stdout}\n\nstderr: {stderr}"); - } - - println!("stdout: {stdout}"); - println!("stderr: {stderr}"); + let assert = cmd.assert().success(); if !python_wasmer_path.exists() { let p = std::fs::read_dir(temp_dir.path()) .unwrap() .filter_map(|e| Some(e.ok()?.path())) .collect::>(); - panic!( - "target {t} was not compiled correctly {stdout} {stderr}, tempdir: {:#?}", - p - ); + let output = assert.get_output(); + panic!("target {t} was not compiled correctly tempdir: {p:#?}, {output:?}",); } } } - - Ok(()) } #[test] -fn run_whoami_works() -> anyhow::Result<()> { +fn run_whoami_works() { // running test locally: should always pass since // developers don't have access to WAPM_DEV_TOKEN if std::env::var("GITHUB_TOKEN").is_err() { - return Ok(()); + return; } let ciuser_token = std::env::var("WAPM_DEV_TOKEN").expect("no CIUSER / WAPM_DEV_TOKEN token"); // Special case: GitHub secrets aren't visible to outside collaborators if ciuser_token.is_empty() { - return Ok(()); + return; } - let output = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg("login") .arg("--registry") .arg("wapm.dev") .arg(ciuser_token) - .output()?; - - if !output.status.success() { - bail!( - "wasmer login failed with: stdout: {}\n\nstderr: {}", - std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"), - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + .assert() + .success(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("whoami") .arg("--registry") .arg("wapm.dev") - .output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if !output.status.success() { - bail!( - "linking failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + .assert() + .success(); - assert_eq!( - stdout, - "logged into registry \"https://registry.wapm.dev/graphql\" as user \"ciuser\"\n" - ); - - Ok(()) + assert + .stdout("logged into registry \"https://registry.wapm.dev/graphql\" as user \"ciuser\"\n"); } #[test] -fn run_wasi_works() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn run_wasi_works() { + let assert = Command::new(get_wasmer_path()) .arg("run") .arg(wasi_test_wasm_path()) .arg("--") .arg("-e") .arg("print(3 * (4 + 5))") - .output()?; - - if !output.status.success() { - bail!( - "linking failed with: stdout: {}\n\nstderr: {}", - std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"), - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + .assert() + .success(); - let stdout_output = std::str::from_utf8(&output.stdout).unwrap(); - assert_eq!(stdout_output, "27\n"); - - Ok(()) + assert.stdout("27\n"); } /// TODO: on linux-musl, the packaging of libwasmer.a doesn't work properly /// Tracked in https://github.com/wasmerio/wasmer/issues/3271 -#[cfg(not(any(target_env = "musl", target_os = "windows")))] -#[cfg(feature = "webc_runner")] +#[cfg_attr(any(target_env = "musl", target_os = "windows"), ignore)] #[test] -fn test_wasmer_create_exe_pirita_works() -> anyhow::Result<()> { +fn test_wasmer_create_exe_pirita_works() { // let temp_dir = Path::new("debug"); // std::fs::create_dir_all(&temp_dir); use wasmer_integration_tests_cli::get_repo_root_path; - let temp_dir = tempfile::TempDir::new()?; + let temp_dir = tempfile::TempDir::new().unwrap(); let temp_dir = temp_dir.path().to_path_buf(); let python_wasmer_path = temp_dir.join("python.wasmer"); - std::fs::copy(wasi_test_python_path(), &python_wasmer_path)?; + std::fs::copy(wasi_test_python_path(), &python_wasmer_path).unwrap(); let python_exe_output_path = temp_dir.join("python"); let native_target = target_lexicon::HOST; @@ -336,33 +263,16 @@ fn test_wasmer_create_exe_pirita_works() -> anyhow::Result<()> { // // cmd.arg("--debug-dir"); // cmd.arg(&temp_dir); - println!("running: {cmd:?}"); - - let output = cmd - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output()?; - - if !output.status.success() { - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - bail!( - "running wasmer create-exe {} failed with: stdout: {}\n\nstderr: {}", - python_wasmer_path.display(), - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + + cmd.assert().success(); println!("compilation ok!"); if !python_exe_output_path.exists() { - return Err(anyhow::anyhow!( + panic!( "python_exe_output_path {} does not exist", python_exe_output_path.display() - )); + ); } println!("invoking command..."); @@ -371,186 +281,103 @@ fn test_wasmer_create_exe_pirita_works() -> anyhow::Result<()> { command.arg("-c"); command.arg("print(\"hello\")"); - let output = command - .output() - .map_err(|e| anyhow::anyhow!("{e}: {command:?}"))?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "1 running python.wasmer failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + command.assert().success().stdout("hello\n"); } // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 -#[cfg(feature = "webc_runner")] #[test] #[ignore] -fn test_wasmer_run_pirita_works() -> anyhow::Result<()> { - let temp_dir = tempfile::TempDir::new()?; +fn test_wasmer_run_pirita_works() { + let temp_dir = tempfile::TempDir::new().unwrap(); let python_wasmer_path = temp_dir.path().join("python.wasmer"); - std::fs::copy(wasi_test_python_path(), &python_wasmer_path)?; + std::fs::copy(wasi_test_python_path(), &python_wasmer_path).unwrap(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg(python_wasmer_path) .arg("--") .arg("-c") .arg("print(\"hello\")") - .output()?; + .assert() + .success(); - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "1 running python.wasmer failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + assert.stdout("hello\n"); } // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 -#[cfg(feature = "webc_runner")] #[test] #[ignore] -fn test_wasmer_run_pirita_url_works() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn test_wasmer_run_pirita_url_works() { + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("https://wapm.dev/syrusakbary/python") .arg("--") .arg("-c") .arg("print(\"hello\")") - .output()?; + .assert() + .success(); - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "1 running python.wasmer failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + assert.stdout("hello\n"); } #[test] -fn test_wasmer_run_works_with_dir() -> anyhow::Result<()> { - let temp_dir = tempfile::TempDir::new()?; +fn test_wasmer_run_works_with_dir() { + let temp_dir = tempfile::TempDir::new().unwrap(); let qjs_path = temp_dir.path().join("qjs.wasm"); - std::fs::copy(wasi_test_wasm_path(), &qjs_path)?; + std::fs::copy(wasi_test_wasm_path(), &qjs_path).unwrap(); std::fs::copy( format!("{}/{}", C_ASSET_PATH, "qjs-wasmer.toml"), temp_dir.path().join("wasmer.toml"), - )?; + ) + .unwrap(); assert!(temp_dir.path().exists()); assert!(temp_dir.path().join("wasmer.toml").exists()); assert!(temp_dir.path().join("qjs.wasm").exists()); // test with "wasmer qjs.wasm" - let output = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg(temp_dir.path()) .arg("--") .arg("--quit") - .output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if !output.status.success() { - bail!( - "running {} failed with: stdout: {}\n\nstderr: {}", - qjs_path.display(), - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + .assert() + .success(); // test again with "wasmer run qjs.wasm" - let output = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg("run") .arg(temp_dir.path()) .arg("--") .arg("--quit") - .output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if !output.status.success() { - bail!( - "running {} failed with: stdout: {}\n\nstderr: {}", - qjs_path.display(), - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + .assert() + .success(); } // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 #[ignore] -#[cfg(not(target_env = "musl"))] +#[cfg_attr(target_env = "musl", ignore)] #[test] -fn test_wasmer_run_works() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn test_wasmer_run_works() { + let assert = Command::new(get_wasmer_path()) .arg("https://wapm.io/python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); + .assert() + .success(); - if stdout != "hello\n" { - bail!( - "1 running python/python failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + assert.stdout("hello\n"); // same test again, but this time with "wasmer run ..." - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("https://wapm.io/python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "2 running python/python failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + assert.stdout("hello\n"); // set wapm.io as the current registry let _ = Command::new(get_wasmer_path()) @@ -559,86 +386,54 @@ fn test_wasmer_run_works() -> anyhow::Result<()> { .arg("wapm.io") // will fail, but set wapm.io as the current registry regardless .arg("öladkfjasöldfkjasdölfkj") - .output()?; + .assert() + .success(); // same test again, but this time without specifying the registry - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "3 running python/python failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + assert.stdout("hello\n"); // same test again, but this time with only the command "python" (should be looked up locally) - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("_/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; - - let stdout = std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"); - - if stdout != "hello\n" { - bail!( - "4 running python/python failed with: stdout: {}\n\nstderr: {}", - stdout, - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } + .assert() + .success(); - Ok(()) + assert.stdout("hello\n"); } #[test] -fn run_no_imports_wasm_works() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn run_no_imports_wasm_works() { + Command::new(get_wasmer_path()) .arg("run") .arg(test_no_imports_wat_path()) - .output()?; - - if !output.status.success() { - bail!( - "linking failed with: stdout: {}\n\nstderr: {}", - std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"), - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + .assert() + .success(); } #[test] fn run_wasi_works_non_existent() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("run") .arg("does/not/exist") - .output()?; - - let stderr = std::str::from_utf8(&output.stderr).unwrap(); - - let stderr_lines = stderr.lines().map(|s| s.to_string()).collect::>(); + .assert() + .failure(); - assert_eq!( - stderr_lines, - vec!["error: Could not find local file does/not/exist".to_string()] - ); + assert + .stderr(contains("error: Unable to resolve \"does/not/exist@*\"")) + .stderr(contains( + "╰─▶ 1: Unable to find any packages that satisfy the query", + )); Ok(()) } @@ -646,128 +441,109 @@ fn run_wasi_works_non_existent() -> anyhow::Result<()> { // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 #[ignore] #[test] -fn run_test_caching_works_for_packages() -> anyhow::Result<()> { +fn run_test_caching_works_for_packages() { // set wapm.io as the current registry - let _ = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg("login") .arg("--registry") .arg("wapm.io") // will fail, but set wapm.io as the current registry regardless .arg("öladkfjasöldfkjasdölfkj") - .output()?; + .assert() + .success(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - if output.stdout != b"hello\n".to_vec() { - panic!("failed to run https://wapm.io/python/python for the first time: stdout = {}, stderr = {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - } + assert.stdout("hello\n"); let time = std::time::Instant::now(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - if output.stdout != b"hello\n".to_vec() { - panic!("failed to run https://wapm.io/python/python for the second time"); - } + assert.stdout("hello\n"); // package should be cached assert!(std::time::Instant::now() - time < std::time::Duration::from_secs(1)); - - Ok(()) } #[test] -fn run_test_caching_works_for_packages_with_versions() -> anyhow::Result<()> { +fn run_test_caching_works_for_packages_with_versions() { // set wapm.io as the current registry - let _ = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg("login") .arg("--registry") .arg("wapm.io") // will fail, but set wapm.io as the current registry regardless .arg("öladkfjasöldfkjasdölfkj") - .output()?; + .assert() + .success(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("python/python@0.1.0") - .arg(format!("--mapdir=.:{}", ASSET_PATH)) - .arg("test.py") - .output()?; + .arg(format!("--mapdir=/app:{}", ASSET_PATH)) + .arg("/app/test.py") + .assert() + .success(); - assert_eq!( - String::from_utf8_lossy(&output.stdout), - "hello\n", - "failed to run https://wapm.io/python/python for the first time" - ); + assert.stdout("hello\n"); let time = std::time::Instant::now(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("python/python@0.1.0") - .arg(format!("--mapdir=.:{}", ASSET_PATH)) - .arg("test.py") - .output()?; - - dbg!(&output); + .arg(format!("--mapdir=/app:{}", ASSET_PATH)) + .arg("/app/test.py") + .assert() + .success(); - assert_eq!( - String::from_utf8_lossy(&output.stdout), - "hello\n", - "failed to run https://wapm.io/python/python for the second time" - ); + assert.stdout("hello\n"); // package should be cached assert!(std::time::Instant::now() - time < std::time::Duration::from_secs(1)); - - Ok(()) } // FIXME: Re-enable. See https://github.com/wasmerio/wasmer/issues/3717 #[ignore] #[test] -fn run_test_caching_works_for_urls() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn run_test_caching_works_for_urls() { + let assert = Command::new(get_wasmer_path()) .arg("https://wapm.io/python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - if output.stdout != b"hello\n".to_vec() { - panic!("failed to run https://wapm.io/python/python for the first time"); - } + assert.stdout("hello\n"); let time = std::time::Instant::now(); - let output = Command::new(get_wasmer_path()) + let assert = Command::new(get_wasmer_path()) .arg("https://wapm.io/python/python") .arg(format!("--mapdir=.:{}", ASSET_PATH)) .arg("test.py") - .output()?; + .assert() + .success(); - if output.stdout != b"hello\n".to_vec() { - panic!("failed to run https://wapm.io/python/python for the second time"); - } + assert.stdout("hello\n"); // package should be cached assert!(std::time::Instant::now() - time < std::time::Duration::from_secs(1)); - - Ok(()) } // This test verifies that "wasmer run --invoke _start module.wat" // works the same as "wasmer run module.wat" (without --invoke). #[test] -fn run_invoke_works_with_nomain_wasi() -> anyhow::Result<()> { +fn run_invoke_works_with_nomain_wasi() { // In this example the function "wasi_unstable.arg_sizes_get" // is a function that is imported from the WASI env. let wasi_wat = " @@ -784,52 +560,38 @@ fn run_invoke_works_with_nomain_wasi() -> anyhow::Result<()> { let random = rand::random::(); let module_file = std::env::temp_dir().join(format!("{random}.wat")); std::fs::write(&module_file, wasi_wat.as_bytes()).unwrap(); - let output = Command::new(get_wasmer_path()) + + Command::new(get_wasmer_path()) .arg("run") .arg(&module_file) - .output()?; + .assert() + .success(); - let stderr = std::str::from_utf8(&output.stderr).unwrap().to_string(); - let success = output.status.success(); - if !success { - println!("ERROR in 'wasmer run [module.wat]':\r\n{stderr}"); - panic!(); - } - - let output = Command::new(get_wasmer_path()) + Command::new(get_wasmer_path()) .arg("run") .arg("--invoke") .arg("_start") .arg(&module_file) - .output()?; - - let stderr = std::str::from_utf8(&output.stderr).unwrap().to_string(); - let success = output.status.success(); - if !success { - println!("ERROR in 'wasmer run --invoke _start [module.wat]':\r\n{stderr}"); - panic!(); - } + .assert() + .success(); std::fs::remove_file(&module_file).unwrap(); - Ok(()) } #[test] -fn run_no_start_wasm_report_error() -> anyhow::Result<()> { - let output = Command::new(get_wasmer_path()) +fn run_no_start_wasm_report_error() { + let assert = Command::new(get_wasmer_path()) .arg("run") .arg(test_no_start_wat_path()) - .output()?; + .assert() + .failure(); - assert!(!output.status.success()); - let result = std::str::from_utf8(&output.stderr).unwrap().to_string(); - assert!(result.contains("Can not find any export functions.")); - Ok(()) + assert.stderr(contains("The module doesn't contain a \"_start\" function")); } // Test that wasmer can run a complex path #[test] -fn test_wasmer_run_complex_url() -> anyhow::Result<()> { +fn test_wasmer_run_complex_url() { let wasm_test_path = wasi_test_wasm_path(); let wasm_test_path = wasm_test_path.canonicalize().unwrap_or(wasm_test_path); let mut wasm_test_path = format!("{}", wasm_test_path.display()); @@ -849,29 +611,11 @@ fn test_wasmer_run_complex_url() -> anyhow::Result<()> { ); } - let mut cmd = Command::new(get_wasmer_path()); - cmd.arg("run"); - cmd.arg(wasm_test_path); - cmd.arg("--"); - cmd.arg("-q"); - - let cmd_str = format!("{cmd:?}"); - let output = cmd.output().with_context(|| { - anyhow::anyhow!( - "failed to run {cmd_str} with {}", - get_wasmer_path().display() - ) - })?; - - if !output.status.success() { - bail!( - "wasmer run qjs.wasm failed with: stdout: {}\n\nstderr: {}", - std::str::from_utf8(&output.stdout) - .expect("stdout is not utf8! need to handle arbitrary bytes"), - std::str::from_utf8(&output.stderr) - .expect("stderr is not utf8! need to handle arbitrary bytes") - ); - } - - Ok(()) + Command::new(get_wasmer_path()) + .arg("run") + .arg(wasm_test_path) + .arg("--") + .arg("-q") + .assert() + .success(); } diff --git a/tests/integration/cli/tests/snapshot.rs b/tests/integration/cli/tests/snapshot.rs index 753aa3f896f..5567ce5dc32 100644 --- a/tests/integration/cli/tests/snapshot.rs +++ b/tests/integration/cli/tests/snapshot.rs @@ -10,10 +10,17 @@ use anyhow::Error; use derivative::Derivative; use futures::TryFutureExt; use insta::assert_json_snapshot; +use regex::Regex; +use once_cell::sync::Lazy; use tempfile::NamedTempFile; use wasmer_integration_tests_cli::get_wasmer_path; +/// Logs tend to include unstable things like timestamps and function call +/// durations, so we use this regex to remove them from our snapshot output. +static LOG_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\d+-\d+-\d+T\d+:\d+:\d+\.\d+[^\n]*\n").unwrap()); + #[derive(Derivative, serde::Serialize, serde::Deserialize, Clone)] #[derivative(Debug, PartialEq)] pub struct TestIncludeWeb { @@ -247,6 +254,12 @@ impl TestBuilder { } } +impl Default for TestBuilder { + fn default() -> Self { + TestBuilder::new() + } +} + pub fn wasm_dir() -> PathBuf { std::env::current_dir() .unwrap() @@ -309,12 +322,13 @@ pub fn run_test_with(spec: TestSpec, code: &[u8], with: RunWith) -> TestResult { cmd.arg("--include-webc").arg(pkg.webc.path()); } - let log_level = if spec.debug_output { - "debug" + if spec.debug_output { + let log_level = ["info", "wasmer_wasix=debug", "wasmer_cli=debug"].join(","); + cmd.env("RUST_LOG", log_level); } else { - "never=error" - }; - cmd.env("RUST_LOG", log_level); + cmd.env("RUST_LOG", "off"); + } + cmd.env("RUST_BACKTRACE", "1"); cmd.arg(wasm_path.path()); @@ -395,6 +409,8 @@ pub fn run_test_with(spec: TestSpec, code: &[u8], with: RunWith) -> TestResult { "test.wasm", ); + let stderr = LOG_PATTERN.replace_all(&stderr, "").into_owned(); + TestResult::Success(TestOutput { stdout, stderr, @@ -1233,3 +1249,12 @@ fn test_snapshot_quickjs() { .run_wasm(include_bytes!("./wasm/qjs.wasm")); assert_json_snapshot!(snapshot); } + +#[test] +fn replace_log_lines() { + let src = "2023-05-29T11:12:50.466396Z\n2023-05-29T11:12:50.466396Z This is a log message\nthis is not"; + + let replaced = LOG_PATTERN.replace_all(src, "").into_owned(); + + assert_eq!(replaced, "this is not"); +}