use crate::{
commands::Command,
context::{Context, StoreContext},
display::{DisplayExportedFuncs, DisplaySequence, DisplayValue},
utils,
};
#[cfg(feature = "wasi")]
use anyhow::Context as _;
use anyhow::{Error, Result, anyhow, bail};
use clap::{Parser, ValueEnum};
#[cfg(feature = "wasi")]
use std::path::PathBuf;
#[cfg(feature = "wasi")]
use std::{net::SocketAddr, str::FromStr};
use std::{path::Path, process};
use wasmi::{Func, Val};
#[cfg(feature = "wasi")]
use wasmi_wasi::{Dir, TcpListener, WasiCtxBuilder, ambient_authority};
#[derive(Parser)]
pub struct RunCommand {
#[clap(
long = "dir",
value_name = "DIRECTORY",
action = clap::ArgAction::Append,
value_hint = clap::ValueHint::DirPath,
)]
#[cfg(feature = "wasi")]
dirs: Vec<PathBuf>,
#[clap(
long = "tcplisten",
value_name = "SOCKET ADDRESS",
action = clap::ArgAction::Append,
)]
#[cfg(feature = "wasi")]
tcplisten: Vec<SocketAddr>,
#[clap(
long = "env",
value_name = "NAME=VAL",
value_parser(KeyValue::from_str),
action = clap::ArgAction::Append,
)]
#[cfg(feature = "wasi")]
envs: Vec<KeyValue>,
#[clap(long = "invoke", value_name = "FUNCTION")]
invoke: Option<String>,
#[clap(long = "compilation-mode", value_enum, default_value_t=CompilationMode::LazyTranslation)]
compilation_mode: CompilationMode,
#[clap(long = "fuel", value_name = "N")]
fuel: Option<u64>,
#[clap(long = "verbose")]
verbose: bool,
#[clap(required = true, value_name = "ARGS", trailing_var_arg = true)]
module_and_args: Vec<String>,
}
impl Command for RunCommand {
fn execute(self) -> Result<(), Error> {
let wasm_file = self.module();
let wasi_ctx = self.store_context()?;
let mut ctx = Context::new(wasm_file, wasi_ctx, self.fuel(), self.compilation_mode())?;
let (func_name, func) = self.invoked_name_and_func(&ctx)?;
let ty = func.ty(ctx.store());
let func_args = utils::decode_func_args(&ty, self.args())?;
let mut func_results = utils::prepare_func_results(&ty);
utils::typecheck_args(&func_name, &ty, &func_args)?;
self.print_execution_start(&func_name, &func_args);
match func.call(ctx.store_mut(), &func_args, &mut func_results) {
Ok(()) => {
self.print_remaining_fuel(&ctx);
Self::print_pretty_results(&func_results);
Ok(())
}
Err(error) => {
if let Some(exit_code) = error.i32_exit_status() {
self.print_remaining_fuel(&ctx);
Self::print_pretty_results(&func_results);
process::exit(exit_code)
}
bail!("failed during execution of {func_name}: {error}")
}
}
}
}
impl RunCommand {
fn invoked_name_and_func(&self, ctx: &Context) -> Result<(String, Func), Error> {
match self.invoked() {
Some(func_name) => {
let func = ctx
.get_func(func_name)
.map_err(|error| anyhow!("{error}\n\n{}", DisplayExportedFuncs::from(ctx)))?;
let func_name = func_name.into();
Ok((func_name, func))
}
None => {
if let Ok(func) = ctx.get_func("") {
Ok(("".into(), func))
} else if let Ok(func) = ctx.get_func("_start") {
Ok(("_start".into(), func))
} else {
bail!(
"did not specify `--invoke` and could not find exported WASI entry point functions\n\n{}",
DisplayExportedFuncs::from(ctx)
)
}
}
}
}
fn print_execution_start(&self, func_name: &str, func_args: &[Val]) {
if !self.verbose() {
return;
}
let module = self.module();
println!(
"executing File({module:?})::{func_name}({}) ...",
DisplaySequence::new(", ", func_args.iter().map(DisplayValue::from))
);
}
fn print_remaining_fuel(&self, ctx: &Context) {
if let Some(given_fuel) = self.fuel() {
let remaining = ctx
.store()
.get_fuel()
.unwrap_or_else(|error| panic!("could not get the remaining fuel: {error}"));
let consumed = given_fuel.saturating_sub(remaining);
println!("fuel consumed: {consumed}, fuel remaining: {remaining}");
}
}
fn print_pretty_results(results: &[Val]) {
for result in results {
println!("{}", DisplayValue::from(result))
}
}
fn module(&self) -> &Path {
Path::new(&self.module_and_args[0])
}
fn invoked(&self) -> Option<&str> {
self.invoke.as_deref()
}
fn args(&self) -> &[String] {
&self.module_and_args[1..]
}
fn fuel(&self) -> Option<u64> {
self.fuel
}
fn compilation_mode(&self) -> wasmi::CompilationMode {
self.compilation_mode.into()
}
fn verbose(&self) -> bool {
self.verbose
}
}
#[cfg(not(feature = "wasi"))]
impl RunCommand {
pub fn store_context(&self) -> Result<StoreContext, Error> {
Ok(StoreContext)
}
}
#[cfg(feature = "wasi")]
impl RunCommand {
pub fn store_context(&self) -> Result<StoreContext, Error> {
let mut wasi_builder = WasiCtxBuilder::new();
for key_val in &self.envs {
wasi_builder.env(key_val.key(), key_val.value())?;
}
wasi_builder.args(self.argv())?;
wasi_builder.inherit_stdio();
for (socket, num_fd) in self.preopen_sockets()?.into_iter().zip(3..) {
wasi_builder.preopened_socket(num_fd, socket)?;
}
for (dir_name, dir) in self.preopen_dirs()? {
wasi_builder.preopened_dir(dir, dir_name)?;
}
Ok(wasi_builder.build())
}
fn preopen_dirs(&self) -> Result<Vec<(&Path, Dir)>> {
self.dirs
.iter()
.map(|path| {
let dir = Dir::open_ambient_dir(path, ambient_authority()).with_context(|| {
format!("failed to open directory '{path:?}' with ambient authority")
})?;
Ok((path.as_ref(), dir))
})
.collect::<Result<Vec<_>>>()
}
fn preopen_sockets(&self) -> Result<Vec<TcpListener>> {
self.tcplisten
.iter()
.map(|addr| {
let std_tcp_listener = std::net::TcpListener::bind(addr)
.with_context(|| format!("failed to bind to tcp address '{addr}'"))?;
std_tcp_listener.set_nonblocking(true)?;
Ok(TcpListener::from_std(std_tcp_listener))
})
.collect::<Result<Vec<_>>>()
}
fn argv(&self) -> &[String] {
&self.module_and_args[..]
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "wasi")]
struct KeyValue {
key_val: Box<str>,
eq_pos: usize,
}
#[cfg(feature = "wasi")]
impl KeyValue {
pub fn key(&self) -> &str {
self.key_val.split_at(self.eq_pos).0
}
pub fn value(&self) -> &str {
&self.key_val.split_at(self.eq_pos).1[1..]
}
}
#[cfg(feature = "wasi")]
impl FromStr for KeyValue {
type Err = Error;
fn from_str(contents: &str) -> Result<Self, Self::Err> {
let Some(eq_pos) = contents.find('=') else {
bail!("missing '=' in KEY=VAL pair: {contents}")
};
let (key, eq_and_value) = contents.split_at(eq_pos);
debug_assert!(eq_and_value.starts_with('='));
let value = &eq_and_value[1..];
if key.is_empty() {
bail!("missing KEY in --env KEY=VAL: {contents}")
}
if value.is_empty() {
bail!("missing VAL in --env KEY=VAL: {contents}")
}
Ok(Self {
key_val: contents.into(),
eq_pos,
})
}
}
#[derive(Debug, Default, Copy, Clone, ValueEnum)]
enum CompilationMode {
Eager,
#[default]
LazyTranslation,
Lazy,
}
impl From<CompilationMode> for wasmi::CompilationMode {
fn from(mode: CompilationMode) -> Self {
match mode {
CompilationMode::Eager => Self::Eager,
CompilationMode::LazyTranslation => Self::LazyTranslation,
CompilationMode::Lazy => Self::Lazy,
}
}
}