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