xref: /wasmtime-44.0.1/src/common.rs (revision b81ef46c)
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                 let mut code = wasmtime::CodeBuilder::new(engine);
231                 code.wasm_binary_or_text(bytes, Some(path))?;
232                 match code.hint() {
233                     Some(wasmtime::CodeHint::Component) => {
234                         #[cfg(feature = "component-model")]
235                         {
236                             self.ensure_allow_components()?;
237                             RunTarget::Component(code.compile_component()?)
238                         }
239                         #[cfg(not(feature = "component-model"))]
240                         {
241                             bail!("support for components was not enabled at compile time");
242                         }
243                     }
244                     Some(wasmtime::CodeHint::Module) | None => {
245                         RunTarget::Core(code.compile_module()?)
246                     }
247                 }
248             }
249 
250             #[cfg(not(any(feature = "cranelift", feature = "winch")))]
251             None => {
252                 let _ = path;
253                 bail!("support for compiling modules was disabled at compile time");
254             }
255         })
256     }
257 
258     pub fn configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()> {
259         // It's ok to block the current thread since we're the only thread in
260         // the program as the CLI. This helps improve the performance of some
261         // blocking operations in WASI, for example, by skipping the
262         // back-and-forth between sync and async.
263         builder.allow_blocking_current_thread(true);
264 
265         if self.common.wasi.inherit_env == Some(true) {
266             for (k, v) in std::env::vars() {
267                 builder.env(&k, &v);
268             }
269         }
270         for (key, value) in self.vars.iter() {
271             let value = match value {
272                 Some(value) => value.clone(),
273                 None => match std::env::var_os(key) {
274                     Some(val) => val
275                         .into_string()
276                         .map_err(|_| anyhow!("environment variable `{key}` not valid utf-8"))?,
277                     None => {
278                         // leave the env var un-set in the guest
279                         continue;
280                     }
281                 },
282             };
283             builder.env(key, &value);
284         }
285 
286         for (host, guest) in self.dirs.iter() {
287             builder.preopened_dir(
288                 host,
289                 guest,
290                 wasmtime_wasi::DirPerms::all(),
291                 wasmtime_wasi::FilePerms::all(),
292             )?;
293         }
294 
295         if self.common.wasi.listenfd == Some(true) {
296             bail!("components do not support --listenfd");
297         }
298         for _ in self.compute_preopen_sockets()? {
299             bail!("components do not support --tcplisten");
300         }
301 
302         if self.common.wasi.inherit_network == Some(true) {
303             builder.inherit_network();
304         }
305         if let Some(enable) = self.common.wasi.allow_ip_name_lookup {
306             builder.allow_ip_name_lookup(enable);
307         }
308         if let Some(enable) = self.common.wasi.tcp {
309             builder.allow_tcp(enable);
310         }
311         if let Some(enable) = self.common.wasi.udp {
312             builder.allow_udp(enable);
313         }
314 
315         Ok(())
316     }
317 
318     pub fn compute_preopen_sockets(&self) -> Result<Vec<TcpListener>> {
319         let mut listeners = vec![];
320 
321         for address in &self.common.wasi.tcplisten {
322             let stdlistener = std::net::TcpListener::bind(address)
323                 .with_context(|| format!("failed to bind to address '{address}'"))?;
324 
325             let _ = stdlistener.set_nonblocking(true)?;
326 
327             listeners.push(stdlistener)
328         }
329         Ok(listeners)
330     }
331 }
332 
333 #[derive(Clone, PartialEq)]
334 pub enum Profile {
335     Native(wasmtime::ProfilingStrategy),
336     Guest { path: String, interval: Duration },
337 }
338 
339 impl Profile {
340     /// Parse the `profile` argument to either the `run` or `serve` commands.
341     pub fn parse(s: &str) -> Result<Profile> {
342         let parts = s.split(',').collect::<Vec<_>>();
343         match &parts[..] {
344             ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)),
345             ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)),
346             ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)),
347             ["guest"] => Ok(Profile::Guest {
348                 path: "wasmtime-guest-profile.json".to_string(),
349                 interval: Duration::from_millis(10),
350             }),
351             ["guest", path] => Ok(Profile::Guest {
352                 path: path.to_string(),
353                 interval: Duration::from_millis(10),
354             }),
355             ["guest", path, dur] => Ok(Profile::Guest {
356                 path: path.to_string(),
357                 interval: WasmtimeOptionValue::parse(Some(dur))?,
358             }),
359             _ => bail!("unknown profiling strategy: {s}"),
360         }
361     }
362 }
363