xref: /wasmtime-44.0.1/src/common.rs (revision cccc4e64)
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