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