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