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 {
unwrap_core(&self) -> &Module31 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")]
unwrap_component(&self) -> &Component40 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
parse_env_var(s: &str) -> Result<(String, Option<String>)>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
parse_dirs(s: &str) -> Result<(String, String)>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 {
store_limits(&self) -> StoreLimits134 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
ensure_allow_precompiled(&self) -> Result<()>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")]
ensure_allow_components(&self) -> Result<()>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
load_module( &self, engine: &Engine, path: &Path, preloaded_bytes: Option<&[u8]>, ) -> Result<RunTarget>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
load_module_contents( &self, engine: &Engine, path: &Path, bytes: &[u8], deserialize_module: impl FnOnce() -> Result<Module>, #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>, ) -> Result<RunTarget>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
configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()>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")]
wasi_http_ctx(&self) -> Result<wasmtime_wasi_http::WasiHttpCtx>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")]
wasi_http_hooks(&self) -> HttpHooks372 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
compute_preopen_sockets(&self) -> Result<Vec<TcpListener>>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
validate_p3_option(&self) -> Result<()>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
validate_cli_enabled(&self) -> Result<Option<bool>>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")]
add_wasmtime_wasi_to_linker<T>( &self, linker: &mut wasmtime::component::Linker<T>, ) -> Result<()> where T: wasmtime_wasi::WasiView,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.
parse(s: &str) -> Result<Profile>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 {
default() -> Self499 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 {
outgoing_body_buffer_chunks(&mut self) -> usize510 fn outgoing_body_buffer_chunks(&mut self) -> usize {
511 self.p2_outgoing_body_buffer_chunks
512 }
513
outgoing_body_chunk_size(&mut self) -> usize514 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