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