xref: /wasmtime-44.0.1/src/common.rs (revision 356c6f8a)
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 {
31     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")]
40     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 
105 fn parse_env_var(s: &str) -> Result<(String, Option<String>)> {
106     let mut parts = s.splitn(2, '=');
107     Ok((
108         parts.next().unwrap().to_string(),
109         parts.next().map(|s| s.to_string()),
110     ))
111 }
112 
113 fn parse_dirs(s: &str) -> Result<(String, String)> {
114     let mut parts = s.split("::");
115     let host = parts.next().unwrap();
116     let guest = match parts.next() {
117         Some(guest) => guest,
118         None => host,
119     };
120     Ok((host.into(), guest.into()))
121 }
122 
123 impl RunCommon {
124     pub fn store_limits(&self) -> StoreLimits {
125         let mut limits = StoreLimitsBuilder::new();
126         if let Some(max) = self.common.wasm.max_memory_size {
127             limits = limits.memory_size(max);
128         }
129         if let Some(max) = self.common.wasm.max_table_elements {
130             limits = limits.table_elements(max);
131         }
132         if let Some(max) = self.common.wasm.max_instances {
133             limits = limits.instances(max);
134         }
135         if let Some(max) = self.common.wasm.max_tables {
136             limits = limits.tables(max);
137         }
138         if let Some(max) = self.common.wasm.max_memories {
139             limits = limits.memories(max);
140         }
141         if let Some(enable) = self.common.wasm.trap_on_grow_failure {
142             limits = limits.trap_on_grow_failure(enable);
143         }
144 
145         limits.build()
146     }
147 
148     pub fn ensure_allow_precompiled(&self) -> Result<()> {
149         if self.allow_precompiled {
150             Ok(())
151         } else {
152             bail!("running a precompiled module requires the `--allow-precompiled` flag")
153         }
154     }
155 
156     #[cfg(feature = "component-model")]
157     fn ensure_allow_components(&self) -> Result<()> {
158         if self.common.wasm.component_model == Some(false) {
159             bail!("cannot execute a component without `--wasm component-model`");
160         }
161 
162         Ok(())
163     }
164 
165     pub fn load_module(&self, engine: &Engine, path: &Path) -> Result<RunTarget> {
166         let path = match path.to_str() {
167             #[cfg(unix)]
168             Some("-") => "/dev/stdin".as_ref(),
169             _ => path,
170         };
171         let file =
172             File::open(path).with_context(|| format!("failed to open wasm module {path:?}"))?;
173 
174         // First attempt to load the module as an mmap. If this succeeds then
175         // detection can be done with the contents of the mmap and if a
176         // precompiled module is detected then `deserialize_file` can be used
177         // which is a slightly more optimal version than `deserialize` since we
178         // can leave most of the bytes on disk until they're referenced.
179         //
180         // If the mmap fails, for example if stdin is a pipe, then fall back to
181         // `std::fs::read` to load the contents. At that point precompiled
182         // modules must go through the `deserialize` functions.
183         //
184         // Note that this has the unfortunate side effect for precompiled
185         // modules on disk that they're opened once to detect what they are and
186         // then again internally in Wasmtime as part of the `deserialize_file`
187         // API. Currently there's no way to pass the `MmapVec` here through to
188         // Wasmtime itself (that'd require making `MmapVec` a public type, both
189         // which isn't ready to happen at this time). It's hoped though that
190         // opening a file twice isn't too bad in the grand scheme of things with
191         // respect to the CLI.
192         match wasmtime::_internal::MmapVec::from_file(file) {
193             Ok(map) => self.load_module_contents(
194                 engine,
195                 path,
196                 &map,
197                 || unsafe { Module::deserialize_file(engine, path) },
198                 #[cfg(feature = "component-model")]
199                 || unsafe { Component::deserialize_file(engine, path) },
200             ),
201             Err(_) => {
202                 let bytes = std::fs::read(path)
203                     .with_context(|| format!("failed to read file: {}", path.display()))?;
204                 self.load_module_contents(
205                     engine,
206                     path,
207                     &bytes,
208                     || unsafe { Module::deserialize(engine, &bytes) },
209                     #[cfg(feature = "component-model")]
210                     || unsafe { Component::deserialize(engine, &bytes) },
211                 )
212             }
213         }
214     }
215 
216     pub fn load_module_contents(
217         &self,
218         engine: &Engine,
219         path: &Path,
220         bytes: &[u8],
221         deserialize_module: impl FnOnce() -> Result<Module>,
222         #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>,
223     ) -> Result<RunTarget> {
224         Ok(match Engine::detect_precompiled(bytes) {
225             Some(Precompiled::Module) => {
226                 self.ensure_allow_precompiled()?;
227                 RunTarget::Core(deserialize_module()?)
228             }
229             #[cfg(feature = "component-model")]
230             Some(Precompiled::Component) => {
231                 self.ensure_allow_precompiled()?;
232                 self.ensure_allow_components()?;
233                 RunTarget::Component(deserialize_component()?)
234             }
235             #[cfg(not(feature = "component-model"))]
236             Some(Precompiled::Component) => {
237                 bail!("support for components was not enabled at compile time");
238             }
239             #[cfg(any(feature = "cranelift", feature = "winch"))]
240             None => {
241                 let mut code = wasmtime::CodeBuilder::new(engine);
242                 code.wasm_binary_or_text(bytes, Some(path))?;
243                 match code.hint() {
244                     Some(wasmtime::CodeHint::Component) => {
245                         #[cfg(feature = "component-model")]
246                         {
247                             self.ensure_allow_components()?;
248                             RunTarget::Component(code.compile_component()?)
249                         }
250                         #[cfg(not(feature = "component-model"))]
251                         {
252                             bail!("support for components was not enabled at compile time");
253                         }
254                     }
255                     Some(wasmtime::CodeHint::Module) | None => {
256                         RunTarget::Core(code.compile_module()?)
257                     }
258                 }
259             }
260 
261             #[cfg(not(any(feature = "cranelift", feature = "winch")))]
262             None => {
263                 let _ = (path, engine);
264                 bail!("support for compiling modules was disabled at compile time");
265             }
266         })
267     }
268 
269     pub fn configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()> {
270         // It's ok to block the current thread since we're the only thread in
271         // the program as the CLI. This helps improve the performance of some
272         // blocking operations in WASI, for example, by skipping the
273         // back-and-forth between sync and async.
274         //
275         // However, do not set this if a timeout is configured, as that would
276         // cause the timeout to be ignored if the guest does, for example,
277         // something like `sleep(FOREVER)`.
278         builder.allow_blocking_current_thread(self.common.wasm.timeout.is_none());
279 
280         if self.common.wasi.inherit_env == Some(true) {
281             for (k, v) in std::env::vars() {
282                 builder.env(&k, &v);
283             }
284         }
285         for (key, value) in self.vars.iter() {
286             let value = match value {
287                 Some(value) => value.clone(),
288                 None => match std::env::var_os(key) {
289                     Some(val) => val
290                         .into_string()
291                         .map_err(|_| format_err!("environment variable `{key}` not valid utf-8"))?,
292                     None => {
293                         // leave the env var un-set in the guest
294                         continue;
295                     }
296                 },
297             };
298             builder.env(key, &value);
299         }
300 
301         for (host, guest) in self.dirs.iter() {
302             builder.preopened_dir(
303                 host,
304                 guest,
305                 wasmtime_wasi::DirPerms::all(),
306                 wasmtime_wasi::FilePerms::all(),
307             )?;
308         }
309 
310         if self.common.wasi.listenfd == Some(true) {
311             bail!("components do not support --listenfd");
312         }
313         for _ in self.compute_preopen_sockets()? {
314             bail!("components do not support --tcplisten");
315         }
316 
317         if self.common.wasi.inherit_network == Some(true) {
318             builder.inherit_network();
319         }
320         if let Some(enable) = self.common.wasi.allow_ip_name_lookup {
321             builder.allow_ip_name_lookup(enable);
322         }
323         if let Some(enable) = self.common.wasi.tcp {
324             builder.allow_tcp(enable);
325         }
326         if let Some(enable) = self.common.wasi.udp {
327             builder.allow_udp(enable);
328         }
329         if let Some(max_size) = self.common.wasi.max_random_size {
330             builder.max_random_size(max_size);
331         }
332 
333         Ok(())
334     }
335 
336     #[cfg(feature = "wasi-http")]
337     pub fn wasi_http_ctx(&self) -> Result<wasmtime_wasi_http::WasiHttpCtx> {
338         let mut http = wasmtime_wasi_http::WasiHttpCtx::new();
339         if let Some(limit) = self.common.wasi.max_http_fields_size {
340             http.set_field_size_limit(limit);
341         }
342         Ok(http)
343     }
344 
345     #[cfg(feature = "wasi-http")]
346     pub fn wasi_http_hooks(&self) -> HttpHooks {
347         HttpHooks {
348             p2_outgoing_body_buffer_chunks: self
349                 .common
350                 .wasi
351                 .http_outgoing_body_buffer_chunks
352                 .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS),
353             p2_outgoing_body_chunk_size: self
354                 .common
355                 .wasi
356                 .http_outgoing_body_chunk_size
357                 .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE),
358         }
359     }
360 
361     pub fn compute_preopen_sockets(&self) -> Result<Vec<TcpListener>> {
362         let mut listeners = vec![];
363 
364         for address in &self.common.wasi.tcplisten {
365             let stdlistener = std::net::TcpListener::bind(address)
366                 .with_context(|| format!("failed to bind to address '{address}'"))?;
367 
368             let _ = stdlistener.set_nonblocking(true)?;
369 
370             listeners.push(stdlistener)
371         }
372         Ok(listeners)
373     }
374 
375     pub fn validate_p3_option(&self) -> Result<()> {
376         let p3 = self.common.wasi.p3.unwrap_or(P3_DEFAULT);
377         if p3 && !cfg!(feature = "component-model-async") {
378             bail!("support for WASIp3 disabled at compile time");
379         }
380         Ok(())
381     }
382 
383     pub fn validate_cli_enabled(&self) -> Result<Option<bool>> {
384         let mut cli = self.common.wasi.cli;
385 
386         // Accept -Scommon as a deprecated alias for -Scli.
387         if let Some(common) = self.common.wasi.common {
388             if cli.is_some() {
389                 bail!(
390                     "The -Scommon option should not be use with -Scli as it is a deprecated alias"
391                 );
392             } else {
393                 // In the future, we may add a warning here to tell users to use
394                 // `-S cli` instead of `-S common`.
395                 cli = Some(common);
396             }
397         }
398 
399         Ok(cli)
400     }
401 
402     /// Adds `wasmtime-wasi` interfaces (dubbed "-Scli" in the flags to the
403     /// `wasmtime` command) to the `linker` provided.
404     ///
405     /// This will handle adding various WASI standard versions to the linker
406     /// internally.
407     #[cfg(feature = "component-model")]
408     pub fn add_wasmtime_wasi_to_linker<T>(
409         &self,
410         linker: &mut wasmtime::component::Linker<T>,
411     ) -> Result<()>
412     where
413         T: wasmtime_wasi::WasiView,
414     {
415         let mut p2_options = wasmtime_wasi::p2::bindings::LinkOptions::default();
416         p2_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false));
417         p2_options.network_error_code(self.common.wasi.network_error_code.unwrap_or(false));
418         wasmtime_wasi::p2::add_to_linker_with_options_async(linker, &p2_options)?;
419 
420         #[cfg(feature = "component-model-async")]
421         if self.common.wasi.p3.unwrap_or(P3_DEFAULT) {
422             let mut p3_options = wasmtime_wasi::p3::bindings::LinkOptions::default();
423             p3_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false));
424             wasmtime_wasi::p3::add_to_linker_with_options(linker, &p3_options)
425                 .context("failed to link `wasi:[email protected]`")?;
426         }
427 
428         Ok(())
429     }
430 }
431 
432 #[derive(Clone, PartialEq)]
433 pub enum Profile {
434     Native(wasmtime::ProfilingStrategy),
435     Guest { path: String, interval: Duration },
436 }
437 
438 impl Profile {
439     /// Parse the `profile` argument to either the `run` or `serve` commands.
440     pub fn parse(s: &str) -> Result<Profile> {
441         let parts = s.split(',').collect::<Vec<_>>();
442         match &parts[..] {
443             ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)),
444             ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)),
445             ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)),
446             ["pulley"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::Pulley)),
447             ["guest"] => Ok(Profile::Guest {
448                 path: "wasmtime-guest-profile.json".to_string(),
449                 interval: Duration::from_millis(10),
450             }),
451             ["guest", path] => Ok(Profile::Guest {
452                 path: path.to_string(),
453                 interval: Duration::from_millis(10),
454             }),
455             ["guest", path, dur] => Ok(Profile::Guest {
456                 path: path.to_string(),
457                 interval: WasmtimeOptionValue::parse(Some(dur))?,
458             }),
459             _ => bail!("unknown profiling strategy: {s}"),
460         }
461     }
462 }
463 
464 #[derive(Copy, Clone, Debug)]
465 #[cfg(feature = "wasi-http")]
466 pub struct HttpHooks {
467     p2_outgoing_body_buffer_chunks: usize,
468     p2_outgoing_body_chunk_size: usize,
469 }
470 
471 #[cfg(feature = "wasi-http")]
472 impl Default for HttpHooks {
473     fn default() -> Self {
474         Self {
475             p2_outgoing_body_buffer_chunks:
476                 wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS,
477             p2_outgoing_body_chunk_size: wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE,
478         }
479     }
480 }
481 
482 #[cfg(feature = "wasi-http")]
483 impl wasmtime_wasi_http::p2::WasiHttpHooks for HttpHooks {
484     fn outgoing_body_buffer_chunks(&mut self) -> usize {
485         self.p2_outgoing_body_buffer_chunks
486     }
487 
488     fn outgoing_body_chunk_size(&mut self) -> usize {
489         self.p2_outgoing_body_chunk_size
490     }
491 }
492 
493 #[cfg(feature = "wasi-http")]
494 impl wasmtime_wasi_http::p3::WasiHttpHooks for HttpHooks {}
495