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