1 //! Common functionality shared between command implementations. 2 3 use clap::Parser; 4 use std::net::TcpListener; 5 use std::{fs::File, path::Path, time::Duration}; 6 use wasmtime::{ 7 Engine, Module, Precompiled, Result, StoreLimits, StoreLimitsBuilder, bail, 8 error::Context as _, format_err, 9 }; 10 use wasmtime_cli_flags::{CommonOptions, opt::WasmtimeOptionValue}; 11 use wasmtime_wasi::WasiCtxBuilder; 12 13 #[cfg(feature = "component-model")] 14 use wasmtime::component::Component; 15 16 /// Whether or not WASIp3 is enabled by default. 17 /// 18 /// Currently this is disabled (the `&& false`), but that'll get removed in the 19 /// future. 20 pub const P3_DEFAULT: bool = cfg!(feature = "component-model-async") && false; 21 22 pub enum RunTarget { 23 Core(Module), 24 25 #[cfg(feature = "component-model")] 26 Component(Component), 27 } 28 29 impl RunTarget { 30 pub fn unwrap_core(&self) -> &Module { 31 match self { 32 RunTarget::Core(module) => module, 33 #[cfg(feature = "component-model")] 34 RunTarget::Component(_) => panic!("expected a core wasm module, not a component"), 35 } 36 } 37 38 #[cfg(feature = "component-model")] 39 pub fn unwrap_component(&self) -> &Component { 40 match self { 41 RunTarget::Component(c) => c, 42 RunTarget::Core(_) => panic!("expected a component, not a core wasm module"), 43 } 44 } 45 } 46 47 /// Common command line arguments for run commands. 48 #[derive(Parser)] 49 pub struct RunCommon { 50 #[command(flatten)] 51 pub common: CommonOptions, 52 53 /// Allow executing precompiled WebAssembly modules as `*.cwasm` files. 54 /// 55 /// Note that this option is not safe to pass if the module being passed in 56 /// is arbitrary user input. Only `wasmtime`-precompiled modules generated 57 /// via the `wasmtime compile` command or equivalent should be passed as an 58 /// argument with this option specified. 59 #[arg(long = "allow-precompiled")] 60 pub allow_precompiled: bool, 61 62 /// Profiling strategy (valid options are: perfmap, jitdump, vtune, guest) 63 /// 64 /// The perfmap, jitdump, and vtune profiling strategies integrate Wasmtime 65 /// with external profilers such as `perf`. The guest profiling strategy 66 /// enables in-process sampling and will write the captured profile to 67 /// `wasmtime-guest-profile.json` by default which can be viewed at 68 /// https://profiler.firefox.com/. 69 /// 70 /// The `guest` option can be additionally configured as: 71 /// 72 /// --profile=guest[,path[,interval]] 73 /// 74 /// where `path` is where to write the profile and `interval` is the 75 /// duration between samples. When used with `--wasm-timeout` the timeout 76 /// will be rounded up to the nearest multiple of this interval. 77 #[arg( 78 long, 79 value_name = "STRATEGY", 80 value_parser = Profile::parse, 81 )] 82 pub profile: Option<Profile>, 83 84 /// Grant access of a host directory to a guest. 85 /// 86 /// If specified as just `HOST_DIR` then the same directory name on the 87 /// host is made available within the guest. If specified as `HOST::GUEST` 88 /// then the `HOST` directory is opened and made available as the name 89 /// `GUEST` in the guest. 90 #[arg(long = "dir", value_name = "HOST_DIR[::GUEST_DIR]", value_parser = parse_dirs)] 91 pub dirs: Vec<(String, String)>, 92 93 /// Pass an environment variable to the program. 94 /// 95 /// The `--env FOO=BAR` form will set the environment variable named `FOO` 96 /// to the value `BAR` for the guest program using WASI. The `--env FOO` 97 /// form will set the environment variable named `FOO` to the same value it 98 /// has in the calling process for the guest, or in other words it will 99 /// cause the environment variable `FOO` to be inherited. 100 #[arg(long = "env", number_of_values = 1, value_name = "NAME[=VAL]", value_parser = parse_env_var)] 101 pub vars: Vec<(String, Option<String>)>, 102 } 103 104 fn parse_env_var(s: &str) -> Result<(String, Option<String>)> { 105 let mut parts = s.splitn(2, '='); 106 Ok(( 107 parts.next().unwrap().to_string(), 108 parts.next().map(|s| s.to_string()), 109 )) 110 } 111 112 fn parse_dirs(s: &str) -> Result<(String, String)> { 113 let mut parts = s.split("::"); 114 let host = parts.next().unwrap(); 115 let guest = match parts.next() { 116 Some(guest) => guest, 117 None => host, 118 }; 119 Ok((host.into(), guest.into())) 120 } 121 122 impl RunCommon { 123 pub fn store_limits(&self) -> StoreLimits { 124 let mut limits = StoreLimitsBuilder::new(); 125 if let Some(max) = self.common.wasm.max_memory_size { 126 limits = limits.memory_size(max); 127 } 128 if let Some(max) = self.common.wasm.max_table_elements { 129 limits = limits.table_elements(max); 130 } 131 if let Some(max) = self.common.wasm.max_instances { 132 limits = limits.instances(max); 133 } 134 if let Some(max) = self.common.wasm.max_tables { 135 limits = limits.tables(max); 136 } 137 if let Some(max) = self.common.wasm.max_memories { 138 limits = limits.memories(max); 139 } 140 if let Some(enable) = self.common.wasm.trap_on_grow_failure { 141 limits = limits.trap_on_grow_failure(enable); 142 } 143 144 limits.build() 145 } 146 147 pub fn ensure_allow_precompiled(&self) -> Result<()> { 148 if self.allow_precompiled { 149 Ok(()) 150 } else { 151 bail!("running a precompiled module requires the `--allow-precompiled` flag") 152 } 153 } 154 155 #[cfg(feature = "component-model")] 156 fn ensure_allow_components(&self) -> Result<()> { 157 if self.common.wasm.component_model == Some(false) { 158 bail!("cannot execute a component without `--wasm component-model`"); 159 } 160 161 Ok(()) 162 } 163 164 pub fn load_module(&self, engine: &Engine, path: &Path) -> Result<RunTarget> { 165 let path = match path.to_str() { 166 #[cfg(unix)] 167 Some("-") => "/dev/stdin".as_ref(), 168 _ => path, 169 }; 170 let file = 171 File::open(path).with_context(|| format!("failed to open wasm module {path:?}"))?; 172 173 // First attempt to load the module as an mmap. If this succeeds then 174 // detection can be done with the contents of the mmap and if a 175 // precompiled module is detected then `deserialize_file` can be used 176 // which is a slightly more optimal version than `deserialize` since we 177 // can leave most of the bytes on disk until they're referenced. 178 // 179 // If the mmap fails, for example if stdin is a pipe, then fall back to 180 // `std::fs::read` to load the contents. At that point precompiled 181 // modules must go through the `deserialize` functions. 182 // 183 // Note that this has the unfortunate side effect for precompiled 184 // modules on disk that they're opened once to detect what they are and 185 // then again internally in Wasmtime as part of the `deserialize_file` 186 // API. Currently there's no way to pass the `MmapVec` here through to 187 // Wasmtime itself (that'd require making `MmapVec` a public type, both 188 // which isn't ready to happen at this time). It's hoped though that 189 // opening a file twice isn't too bad in the grand scheme of things with 190 // respect to the CLI. 191 match wasmtime::_internal::MmapVec::from_file(file) { 192 Ok(map) => self.load_module_contents( 193 engine, 194 path, 195 &map, 196 || unsafe { Module::deserialize_file(engine, path) }, 197 #[cfg(feature = "component-model")] 198 || unsafe { Component::deserialize_file(engine, path) }, 199 ), 200 Err(_) => { 201 let bytes = std::fs::read(path) 202 .with_context(|| format!("failed to read file: {}", path.display()))?; 203 self.load_module_contents( 204 engine, 205 path, 206 &bytes, 207 || unsafe { Module::deserialize(engine, &bytes) }, 208 #[cfg(feature = "component-model")] 209 || unsafe { Component::deserialize(engine, &bytes) }, 210 ) 211 } 212 } 213 } 214 215 pub fn load_module_contents( 216 &self, 217 engine: &Engine, 218 path: &Path, 219 bytes: &[u8], 220 deserialize_module: impl FnOnce() -> Result<Module>, 221 #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>, 222 ) -> Result<RunTarget> { 223 Ok(match Engine::detect_precompiled(bytes) { 224 Some(Precompiled::Module) => { 225 self.ensure_allow_precompiled()?; 226 RunTarget::Core(deserialize_module()?) 227 } 228 #[cfg(feature = "component-model")] 229 Some(Precompiled::Component) => { 230 self.ensure_allow_precompiled()?; 231 self.ensure_allow_components()?; 232 RunTarget::Component(deserialize_component()?) 233 } 234 #[cfg(not(feature = "component-model"))] 235 Some(Precompiled::Component) => { 236 bail!("support for components was not enabled at compile time"); 237 } 238 #[cfg(any(feature = "cranelift", feature = "winch"))] 239 None => { 240 let mut code = wasmtime::CodeBuilder::new(engine); 241 code.wasm_binary_or_text(bytes, Some(path))?; 242 match code.hint() { 243 Some(wasmtime::CodeHint::Component) => { 244 #[cfg(feature = "component-model")] 245 { 246 self.ensure_allow_components()?; 247 RunTarget::Component(code.compile_component()?) 248 } 249 #[cfg(not(feature = "component-model"))] 250 { 251 bail!("support for components was not enabled at compile time"); 252 } 253 } 254 Some(wasmtime::CodeHint::Module) | None => { 255 RunTarget::Core(code.compile_module()?) 256 } 257 } 258 } 259 260 #[cfg(not(any(feature = "cranelift", feature = "winch")))] 261 None => { 262 let _ = (path, engine); 263 bail!("support for compiling modules was disabled at compile time"); 264 } 265 }) 266 } 267 268 pub fn configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()> { 269 // It's ok to block the current thread since we're the only thread in 270 // the program as the CLI. This helps improve the performance of some 271 // blocking operations in WASI, for example, by skipping the 272 // back-and-forth between sync and async. 273 // 274 // However, do not set this if a timeout is configured, as that would 275 // cause the timeout to be ignored if the guest does, for example, 276 // something like `sleep(FOREVER)`. 277 builder.allow_blocking_current_thread(self.common.wasm.timeout.is_none()); 278 279 if self.common.wasi.inherit_env == Some(true) { 280 for (k, v) in std::env::vars() { 281 builder.env(&k, &v); 282 } 283 } 284 for (key, value) in self.vars.iter() { 285 let value = match value { 286 Some(value) => value.clone(), 287 None => match std::env::var_os(key) { 288 Some(val) => val 289 .into_string() 290 .map_err(|_| format_err!("environment variable `{key}` not valid utf-8"))?, 291 None => { 292 // leave the env var un-set in the guest 293 continue; 294 } 295 }, 296 }; 297 builder.env(key, &value); 298 } 299 300 for (host, guest) in self.dirs.iter() { 301 builder.preopened_dir( 302 host, 303 guest, 304 wasmtime_wasi::DirPerms::all(), 305 wasmtime_wasi::FilePerms::all(), 306 )?; 307 } 308 309 if self.common.wasi.listenfd == Some(true) { 310 bail!("components do not support --listenfd"); 311 } 312 for _ in self.compute_preopen_sockets()? { 313 bail!("components do not support --tcplisten"); 314 } 315 316 if self.common.wasi.inherit_network == Some(true) { 317 builder.inherit_network(); 318 } 319 if let Some(enable) = self.common.wasi.allow_ip_name_lookup { 320 builder.allow_ip_name_lookup(enable); 321 } 322 if let Some(enable) = self.common.wasi.tcp { 323 builder.allow_tcp(enable); 324 } 325 if let Some(enable) = self.common.wasi.udp { 326 builder.allow_udp(enable); 327 } 328 329 Ok(()) 330 } 331 332 pub fn compute_preopen_sockets(&self) -> Result<Vec<TcpListener>> { 333 let mut listeners = vec![]; 334 335 for address in &self.common.wasi.tcplisten { 336 let stdlistener = std::net::TcpListener::bind(address) 337 .with_context(|| format!("failed to bind to address '{address}'"))?; 338 339 let _ = stdlistener.set_nonblocking(true)?; 340 341 listeners.push(stdlistener) 342 } 343 Ok(listeners) 344 } 345 346 pub fn validate_p3_option(&self) -> Result<()> { 347 let p3 = self.common.wasi.p3.unwrap_or(P3_DEFAULT); 348 if p3 && !cfg!(feature = "component-model-async") { 349 bail!("support for WASIp3 disabled at compile time"); 350 } 351 Ok(()) 352 } 353 354 pub fn validate_cli_enabled(&self) -> Result<Option<bool>> { 355 let mut cli = self.common.wasi.cli; 356 357 // Accept -Scommon as a deprecated alias for -Scli. 358 if let Some(common) = self.common.wasi.common { 359 if cli.is_some() { 360 bail!( 361 "The -Scommon option should not be use with -Scli as it is a deprecated alias" 362 ); 363 } else { 364 // In the future, we may add a warning here to tell users to use 365 // `-S cli` instead of `-S common`. 366 cli = Some(common); 367 } 368 } 369 370 Ok(cli) 371 } 372 373 /// Adds `wasmtime-wasi` interfaces (dubbed "-Scli" in the flags to the 374 /// `wasmtime` command) to the `linker` provided. 375 /// 376 /// This will handle adding various WASI standard versions to the linker 377 /// internally. 378 #[cfg(feature = "component-model")] 379 pub fn add_wasmtime_wasi_to_linker<T>( 380 &self, 381 linker: &mut wasmtime::component::Linker<T>, 382 ) -> Result<()> 383 where 384 T: wasmtime_wasi::WasiView, 385 { 386 let mut p2_options = wasmtime_wasi::p2::bindings::LinkOptions::default(); 387 p2_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false)); 388 p2_options.network_error_code(self.common.wasi.network_error_code.unwrap_or(false)); 389 wasmtime_wasi::p2::add_to_linker_with_options_async(linker, &p2_options)?; 390 391 #[cfg(feature = "component-model-async")] 392 if self.common.wasi.p3.unwrap_or(P3_DEFAULT) { 393 let mut p3_options = wasmtime_wasi::p3::bindings::LinkOptions::default(); 394 p3_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false)); 395 wasmtime_wasi::p3::add_to_linker_with_options(linker, &p3_options) 396 .context("failed to link `wasi:[email protected]`")?; 397 } 398 399 Ok(()) 400 } 401 } 402 403 #[derive(Clone, PartialEq)] 404 pub enum Profile { 405 Native(wasmtime::ProfilingStrategy), 406 Guest { path: String, interval: Duration }, 407 } 408 409 impl Profile { 410 /// Parse the `profile` argument to either the `run` or `serve` commands. 411 pub fn parse(s: &str) -> Result<Profile> { 412 let parts = s.split(',').collect::<Vec<_>>(); 413 match &parts[..] { 414 ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)), 415 ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)), 416 ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)), 417 ["pulley"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::Pulley)), 418 ["guest"] => Ok(Profile::Guest { 419 path: "wasmtime-guest-profile.json".to_string(), 420 interval: Duration::from_millis(10), 421 }), 422 ["guest", path] => Ok(Profile::Guest { 423 path: path.to_string(), 424 interval: Duration::from_millis(10), 425 }), 426 ["guest", path, dur] => Ok(Profile::Guest { 427 path: path.to_string(), 428 interval: WasmtimeOptionValue::parse(Some(dur))?, 429 }), 430 _ => bail!("unknown profiling strategy: {s}"), 431 } 432 } 433 } 434 435 #[derive(Default, Clone)] 436 #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] 437 pub struct DefaultP3Ctx; 438 #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] 439 impl wasmtime_wasi_http::p3::WasiHttpCtx for DefaultP3Ctx {} 440