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