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