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 = File::open(path)?; 164 165 // First attempt to load the module as an mmap. If this succeeds then 166 // detection can be done with the contents of the mmap and if a 167 // precompiled module is detected then `deserialize_file` can be used 168 // which is a slightly more optimal version than `deserialize` since we 169 // can leave most of the bytes on disk until they're referenced. 170 // 171 // If the mmap fails, for example if stdin is a pipe, then fall back to 172 // `std::fs::read` to load the contents. At that point precompiled 173 // modules must go through the `deserialize` functions. 174 // 175 // Note that this has the unfortunate side effect for precompiled 176 // modules on disk that they're opened once to detect what they are and 177 // then again internally in Wasmtime as part of the `deserialize_file` 178 // API. Currently there's no way to pass the `MmapVec` here through to 179 // Wasmtime itself (that'd require making `MmapVec` a public type, both 180 // which isn't ready to happen at this time). It's hoped though that 181 // opening a file twice isn't too bad in the grand scheme of things with 182 // respect to the CLI. 183 match wasmtime::_internal::MmapVec::from_file(file) { 184 Ok(map) => self.load_module_contents( 185 engine, 186 path, 187 &map, 188 || unsafe { Module::deserialize_file(engine, path) }, 189 #[cfg(feature = "component-model")] 190 || unsafe { Component::deserialize_file(engine, path) }, 191 ), 192 Err(_) => { 193 let bytes = std::fs::read(path) 194 .with_context(|| format!("failed to read file: {}", path.display()))?; 195 self.load_module_contents( 196 engine, 197 path, 198 &bytes, 199 || unsafe { Module::deserialize(engine, &bytes) }, 200 #[cfg(feature = "component-model")] 201 || unsafe { Component::deserialize(engine, &bytes) }, 202 ) 203 } 204 } 205 } 206 207 pub fn load_module_contents( 208 &self, 209 engine: &Engine, 210 path: &Path, 211 bytes: &[u8], 212 deserialize_module: impl FnOnce() -> Result<Module>, 213 #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>, 214 ) -> Result<RunTarget> { 215 Ok(match engine.detect_precompiled(bytes) { 216 Some(Precompiled::Module) => { 217 self.ensure_allow_precompiled()?; 218 RunTarget::Core(deserialize_module()?) 219 } 220 #[cfg(feature = "component-model")] 221 Some(Precompiled::Component) => { 222 self.ensure_allow_precompiled()?; 223 self.ensure_allow_components()?; 224 RunTarget::Component(deserialize_component()?) 225 } 226 #[cfg(not(feature = "component-model"))] 227 Some(Precompiled::Component) => { 228 bail!("support for components was not enabled at compile time"); 229 } 230 #[cfg(any(feature = "cranelift", feature = "winch"))] 231 None => { 232 let mut code = wasmtime::CodeBuilder::new(engine); 233 code.wasm_binary_or_text(bytes, Some(path))?; 234 match code.hint() { 235 Some(wasmtime::CodeHint::Component) => { 236 #[cfg(feature = "component-model")] 237 { 238 self.ensure_allow_components()?; 239 RunTarget::Component(code.compile_component()?) 240 } 241 #[cfg(not(feature = "component-model"))] 242 { 243 bail!("support for components was not enabled at compile time"); 244 } 245 } 246 Some(wasmtime::CodeHint::Module) | None => { 247 RunTarget::Core(code.compile_module()?) 248 } 249 } 250 } 251 252 #[cfg(not(any(feature = "cranelift", feature = "winch")))] 253 None => { 254 let _ = path; 255 bail!("support for compiling modules was disabled at compile time"); 256 } 257 }) 258 } 259 260 pub fn configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()> { 261 // It's ok to block the current thread since we're the only thread in 262 // the program as the CLI. This helps improve the performance of some 263 // blocking operations in WASI, for example, by skipping the 264 // back-and-forth between sync and async. 265 // 266 // However, do not set this if a timeout is configured, as that would 267 // cause the timeout to be ignored if the guest does, for example, 268 // something like `sleep(FOREVER)`. 269 builder.allow_blocking_current_thread(self.common.wasm.timeout.is_none()); 270 271 if self.common.wasi.inherit_env == Some(true) { 272 for (k, v) in std::env::vars() { 273 builder.env(&k, &v); 274 } 275 } 276 for (key, value) in self.vars.iter() { 277 let value = match value { 278 Some(value) => value.clone(), 279 None => match std::env::var_os(key) { 280 Some(val) => val 281 .into_string() 282 .map_err(|_| anyhow!("environment variable `{key}` not valid utf-8"))?, 283 None => { 284 // leave the env var un-set in the guest 285 continue; 286 } 287 }, 288 }; 289 builder.env(key, &value); 290 } 291 292 for (host, guest) in self.dirs.iter() { 293 builder.preopened_dir( 294 host, 295 guest, 296 wasmtime_wasi::DirPerms::all(), 297 wasmtime_wasi::FilePerms::all(), 298 )?; 299 } 300 301 if self.common.wasi.listenfd == Some(true) { 302 bail!("components do not support --listenfd"); 303 } 304 for _ in self.compute_preopen_sockets()? { 305 bail!("components do not support --tcplisten"); 306 } 307 308 if self.common.wasi.inherit_network == Some(true) { 309 builder.inherit_network(); 310 } 311 if let Some(enable) = self.common.wasi.allow_ip_name_lookup { 312 builder.allow_ip_name_lookup(enable); 313 } 314 if let Some(enable) = self.common.wasi.tcp { 315 builder.allow_tcp(enable); 316 } 317 if let Some(enable) = self.common.wasi.udp { 318 builder.allow_udp(enable); 319 } 320 321 Ok(()) 322 } 323 324 pub fn compute_preopen_sockets(&self) -> Result<Vec<TcpListener>> { 325 let mut listeners = vec![]; 326 327 for address in &self.common.wasi.tcplisten { 328 let stdlistener = std::net::TcpListener::bind(address) 329 .with_context(|| format!("failed to bind to address '{address}'"))?; 330 331 let _ = stdlistener.set_nonblocking(true)?; 332 333 listeners.push(stdlistener) 334 } 335 Ok(listeners) 336 } 337 338 pub fn compute_wasi_features(&self) -> LinkOptions { 339 let mut options = LinkOptions::default(); 340 options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false)); 341 options.network_error_code(self.common.wasi.network_error_code.unwrap_or(false)); 342 options 343 } 344 } 345 346 #[derive(Clone, PartialEq)] 347 pub enum Profile { 348 Native(wasmtime::ProfilingStrategy), 349 Guest { path: String, interval: Duration }, 350 } 351 352 impl Profile { 353 /// Parse the `profile` argument to either the `run` or `serve` commands. 354 pub fn parse(s: &str) -> Result<Profile> { 355 let parts = s.split(',').collect::<Vec<_>>(); 356 match &parts[..] { 357 ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)), 358 ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)), 359 ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)), 360 ["pulley"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::Pulley)), 361 ["guest"] => Ok(Profile::Guest { 362 path: "wasmtime-guest-profile.json".to_string(), 363 interval: Duration::from_millis(10), 364 }), 365 ["guest", path] => Ok(Profile::Guest { 366 path: path.to_string(), 367 interval: Duration::from_millis(10), 368 }), 369 ["guest", path, dur] => Ok(Profile::Guest { 370 path: path.to_string(), 371 interval: WasmtimeOptionValue::parse(Some(dur))?, 372 }), 373 _ => bail!("unknown profiling strategy: {s}"), 374 } 375 } 376 } 377