1#![allow(missing_docs, unused)]
2
3mod capabilities;
4mod package_source;
5mod runtime;
6mod target;
7mod wasi;
8
9use std::{
10 borrow::Cow,
11 collections::{BTreeMap, hash_map::DefaultHasher},
12 fmt::{Binary, Display},
13 fs::File,
14 hash::{BuildHasherDefault, Hash, Hasher},
15 io::{ErrorKind, LineWriter, Read, Write},
16 net::SocketAddr,
17 path::{Path, PathBuf},
18 str::FromStr,
19 sync::{Arc, Mutex},
20 time::{Duration, SystemTime, UNIX_EPOCH},
21};
22
23use anyhow::{Context, Error, anyhow, bail};
24use clap::{Parser, ValueEnum};
25use colored::Colorize;
26use futures::future::BoxFuture;
27use indicatif::{MultiProgress, ProgressBar};
28use once_cell::sync::Lazy;
29use tempfile::NamedTempFile;
30use url::Url;
31#[cfg(feature = "sys")]
32use wasmer::sys::NativeEngineExt;
33use wasmer::{
34 AsStoreMut, DeserializeError, Engine, Function, Imports, Instance, Module, RuntimeError, Store,
35 Type, TypedFunction, Value, wat2wasm,
36};
37
38use wasmer_types::{Features, target::Target};
39
40#[cfg(feature = "compiler")]
41use wasmer_compiler::ArtifactBuild;
42use wasmer_config::package::PackageSource;
43use wasmer_package::utils::from_disk;
44use wasmer_types::ModuleHash;
45
46#[cfg(feature = "journal")]
47use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
48use wasmer_wasix::{
49 Runtime, SpawnError, WasiError,
50 bin_factory::{BinaryPackage, BinaryPackageCommand},
51 journal::CompactingLogFileJournal,
52 runners::{
53 MappedCommand, MappedDirectory, Runner,
54 wasi::{RuntimeOrEngine, WasiRunner},
55 },
56 runtime::{
57 OverriddenRuntime,
58 module_cache::{CacheError, HashedModuleData},
59 package_loader::PackageLoader,
60 resolver::QueryError,
61 task_manager::VirtualTaskManagerExt,
62 },
63};
64use webc::Container;
65use webc::metadata::Manifest;
66
67use crate::{
68 backend::RuntimeOptions,
69 commands::run::{target::TargetOnDisk, wasi::Wasi},
70 config::WasmerEnv,
71 error::PrettyError,
72 logging::Output,
73};
74
75use self::{
76 package_source::CliPackageSource, runtime::MonitoringRuntime, target::ExecutableTarget,
77};
78
79const TICK: Duration = Duration::from_millis(250);
80
81#[derive(Debug, Parser)]
83pub struct Run {
84 #[clap(flatten)]
85 env: WasmerEnv,
86 #[clap(flatten)]
87 rt: RuntimeOptions,
88 #[clap(flatten)]
89 wasi: crate::commands::run::Wasi,
90 #[clap(long = "stack-size")]
92 stack_size: Option<usize>,
93 #[clap(short, long, aliases = &["command", "command-name"])]
95 entrypoint: Option<String>,
96 #[clap(short, long)]
98 invoke: Option<String>,
99 #[clap(name = "COREDUMP_PATH", long)]
101 coredump_on_trap: Option<PathBuf>,
102 #[clap(long = "experimental-napi")]
104 experimental_napi: bool,
105 #[clap(value_parser = CliPackageSource::infer)]
107 input: CliPackageSource,
108 args: Vec<String>,
110}
111
112impl Run {
113 #[cfg(feature = "napi-v8")]
114 fn module_needs_napi(module: &Module) -> bool {
115 let (napi_version, napi_extension_version) = wasmer_napi::module_needs_napi(module);
116 napi_version.is_some() || napi_extension_version.is_some()
117 }
118
119 #[cfg(feature = "napi-v8")]
120 fn maybe_wrap_runtime_with_napi(
121 &self,
122 module: &Module,
123 runtime: Arc<dyn Runtime + Send + Sync>,
124 ) -> Result<Arc<dyn Runtime + Send + Sync>, Error> {
125 use anyhow::ensure;
126
127 if !Self::module_needs_napi(module) {
128 return Ok(runtime);
129 }
130 ensure!(
131 self.experimental_napi,
132 "This module imports N-API. Re-run with '--experimental-napi' to enable the experimental N-API runtime."
133 );
134
135 let hooks = wasmer_napi::NapiCtx::default().runtime_hooks();
136 Ok(Arc::new(
137 OverriddenRuntime::new(runtime)
138 .with_additional_imports({
139 let hooks = hooks.clone();
140 move |module, store| hooks.additional_imports(module, store)
141 })
142 .with_instance_setup(move |module, store, instance, imported_memory| {
143 hooks.configure_instance(module, store, instance, imported_memory)
144 }),
145 ))
146 }
147
148 #[cfg(feature = "napi-v8")]
149 fn configure_wasi_runner_for_napi(&self, module: &Module, runner: &mut WasiRunner) {
150 if Self::module_needs_napi(module) {
151 runner
152 .capabilities_mut()
153 .threading
154 .enable_asynchronous_threading = false;
155 }
156 }
157
158 #[cfg(not(feature = "napi-v8"))]
159 fn maybe_wrap_runtime_with_napi(
160 &self,
161 _module: &Module,
162 runtime: Arc<dyn Runtime + Send + Sync>,
163 ) -> Result<Arc<dyn Runtime + Send + Sync>, Error> {
164 Ok(runtime)
165 }
166
167 #[cfg(not(feature = "napi-v8"))]
168 fn configure_wasi_runner_for_napi(&self, _module: &Module, _runner: &mut WasiRunner) {}
169
170 pub fn execute(self, output: Output) -> ! {
171 let result = self.execute_inner(output);
172 exit_with_wasi_exit_code(result);
173 }
174
175 #[tracing::instrument(level = "debug", name = "wasmer_run", skip_all)]
176 fn execute_inner(mut self, output: Output) -> Result<(), Error> {
177 self.print_option_warnings();
178
179 let pb = ProgressBar::new_spinner();
180 pb.set_draw_target(output.draw_target());
181 pb.enable_steady_tick(TICK);
182
183 pb.set_message("Initializing the WebAssembly VM");
184
185 let runtime = tokio::runtime::Builder::new_multi_thread()
186 .enable_all()
187 .build()?;
188 let handle = runtime.handle().clone();
189
190 let webc_version_var = std::env::var("WASMER_WEBC_VERSION");
193 let preferred_webc_version = match webc_version_var.as_deref() {
194 Ok("2") => webc::Version::V2,
195 Ok("3") | Err(_) => webc::Version::V3,
196 Ok(other) => {
197 bail!("unknown webc version: '{other}'");
198 }
199 };
200
201 let _guard = handle.enter();
202
203 let mut wasm_bytes: Option<Vec<u8>> = None;
205
206 tracing::info!("Input source: {:?}", self.input);
208 if let CliPackageSource::File(path) = &self.input {
209 tracing::info!("Input file path: {}", path.display());
210
211 let target = TargetOnDisk::from_file(path);
213 if let Ok(target) = target {
214 match target {
215 TargetOnDisk::WebAssemblyBinary => {
216 if let Ok(data) = std::fs::read(path) {
217 wasm_bytes = Some(data);
218 } else {
219 tracing::info!("Failed to read file: {}", path.display());
220 }
221 }
222 TargetOnDisk::Wat => match std::fs::read(path) {
223 Ok(data) => match wat2wasm(&data) {
224 Ok(wasm) => {
225 wasm_bytes = Some(wasm.to_vec());
226 }
227 Err(e) => {
228 tracing::info!(
229 "Failed to convert WAT to Wasm for {}: {e}",
230 path.display()
231 );
232 }
233 },
234 Err(e) => {
235 tracing::info!("Failed to read WAT file {}: {e}", path.display());
236 }
237 },
238 _ => {}
239 }
240 } else {
241 tracing::info!(
242 "Failed to read file for feature detection: {}",
243 path.display()
244 );
245 }
246 } else {
247 tracing::info!("Input is not a file, skipping WebAssembly feature detection");
248 }
249
250 let mut engine = match &wasm_bytes {
252 Some(wasm_bytes) => {
253 tracing::info!("Attempting to detect WebAssembly features from binary");
254
255 self.rt
256 .get_engine_for_module(wasm_bytes, &Target::default())?
257 }
258 None => {
259 if let CliPackageSource::Package(pkg_source) = &self.input {
261 tracing::info!("Checking package for WebAssembly features: {}", pkg_source);
262 self.rt.get_engine(&Target::default())?
263 } else {
264 tracing::info!("No feature detection possible, using default engine");
265 self.rt.get_engine(&Target::default())?
266 }
267 }
268 };
269
270 let engine_kind = engine.deterministic_id();
271 tracing::info!("Executing on backend {engine_kind:?}");
272
273 #[cfg(feature = "sys")]
274 if engine.is_sys() && self.stack_size.is_some() {
275 wasmer_vm::set_stack_size(self.stack_size.unwrap());
276 }
277
278 let engine = engine.clone();
279
280 let runtime = self.wasi.prepare_runtime(
281 engine,
282 &self.env,
283 &capabilities::get_capability_cache_path(&self.env, &self.input)?,
284 runtime,
285 preferred_webc_version,
286 self.rt.compiler_debug_dir.is_some(),
287 )?;
288
289 let monitoring_runtime = Arc::new(MonitoringRuntime::new(
292 runtime,
293 pb.clone(),
294 output.is_quiet_or_no_tty(),
295 ));
296 let runtime: Arc<dyn Runtime + Send + Sync> = monitoring_runtime.runtime.clone();
297 let monitoring_runtime: Arc<dyn Runtime + Send + Sync> = monitoring_runtime;
298
299 let target = self.input.resolve_target(&monitoring_runtime, &pb)?;
300
301 if let ExecutableTarget::Package(ref pkg) = target {
302 self.wasi
303 .volumes
304 .extend(pkg.additional_host_mapped_directories.clone());
305 }
306
307 pb.finish_and_clear();
308
309 let tty = runtime.tty().map(|tty| tty.tty_get());
311
312 let result = {
313 match target {
314 ExecutableTarget::WebAssembly {
315 module,
316 module_hash,
317 path,
318 } => self.execute_wasm(&path, module, module_hash, runtime.clone()),
319 ExecutableTarget::Package(pkg) => {
320 if let Some(cmd) = pkg.get_entrypoint_command()
322 && let Some(features) = cmd.wasm_features()
323 {
324 let backends = self.rt.get_available_backends()?;
326 let available_engines = backends
327 .iter()
328 .map(|b| b.to_string())
329 .collect::<Vec<_>>()
330 .join(", ");
331
332 let filtered_backends = RuntimeOptions::filter_backends_by_features(
333 backends.clone(),
334 &features,
335 &Target::default(),
336 );
337
338 if let Some(backend) = filtered_backends.first() {
339 let engine_id = backend.to_string();
340
341 if let Ok(new_engine) = backend.get_engine(&Target::default(), &self.rt)
343 {
344 tracing::info!(
345 "The command '{}' requires to run the Wasm module with the features {:?}. The backends available are {}. Choosing {}.",
346 cmd.name(),
347 features,
348 available_engines,
349 engine_id
350 );
351 let capability_cache_path =
353 capabilities::get_capability_cache_path(
354 &self.env,
355 &self.input,
356 )?;
357 let new_runtime = self.wasi.prepare_runtime(
358 new_engine,
359 &self.env,
360 &capability_cache_path,
361 tokio::runtime::Builder::new_multi_thread()
362 .enable_all()
363 .build()?,
364 preferred_webc_version,
365 self.rt.compiler_debug_dir.is_some(),
366 )?;
367
368 let new_runtime = Arc::new(MonitoringRuntime::new(
369 new_runtime,
370 pb.clone(),
371 output.is_quiet_or_no_tty(),
372 ));
373 return self.execute_webc(&pkg, new_runtime);
374 }
375 }
376 }
377 self.execute_webc(&pkg, monitoring_runtime)
378 }
379 }
380 };
381
382 if let Some(state) = tty
384 && let Some(tty) = runtime.tty()
385 {
386 tty.tty_set(state);
387 }
388
389 if let Err(e) = &result {
390 self.maybe_save_coredump(e);
391 }
392
393 result
394 }
395
396 #[tracing::instrument(skip_all)]
397 fn execute_wasm(
398 &self,
399 path: &Path,
400 module: Module,
401 module_hash: ModuleHash,
402 runtime: Arc<dyn Runtime + Send + Sync>,
403 ) -> Result<(), Error> {
404 if wasmer_wasix::is_wasi_module(&module) || wasmer_wasix::is_wasix_module(&module) {
405 self.execute_wasi_module(path, module, module_hash, runtime)
406 } else {
407 self.execute_pure_wasm_module(&module)
408 }
409 }
410
411 #[tracing::instrument(skip_all)]
412 fn execute_webc(
413 &self,
414 pkg: &BinaryPackage,
415 runtime: Arc<dyn Runtime + Send + Sync>,
416 ) -> Result<(), Error> {
417 let id = match self.entrypoint.as_deref() {
418 Some(cmd) => cmd,
419 None => pkg.infer_entrypoint()?,
420 };
421 let cmd = pkg
422 .get_command(id)
423 .with_context(|| format!("Unable to get metadata for the \"{id}\" command"))?;
424
425 let uses = self.load_injected_packages(&runtime)?;
426
427 if WasiRunner::can_run_command(cmd.metadata())? {
428 self.run_wasi(id, pkg, uses, runtime)
429 } else {
430 bail!(
431 "Unable to find a runner that supports \"{}\"",
432 cmd.metadata().runner
433 );
434 }
435 }
436
437 #[tracing::instrument(level = "debug", skip_all)]
438 fn load_injected_packages(
439 &self,
440 runtime: &Arc<dyn Runtime + Send + Sync>,
441 ) -> Result<Vec<BinaryPackage>, Error> {
442 let mut dependencies = Vec::new();
443
444 for name in &self.wasi.uses {
445 let specifier = name
446 .parse::<PackageSource>()
447 .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
448 let pkg = {
449 let specifier = specifier.clone();
450 let inner_runtime = runtime.clone();
451 runtime
452 .task_manager()
453 .spawn_and_block_on(async move {
454 BinaryPackage::from_registry(&specifier, inner_runtime.as_ref()).await
455 })
456 .with_context(|| format!("Unable to load \"{name}\""))??
457 };
458 dependencies.push(pkg);
459 }
460
461 Ok(dependencies)
462 }
463
464 fn run_wasi(
465 &self,
466 command_name: &str,
467 pkg: &BinaryPackage,
468 uses: Vec<BinaryPackage>,
469 runtime: Arc<dyn Runtime + Send + Sync>,
470 ) -> Result<(), Error> {
471 #[cfg(feature = "napi-v8")]
472 let (module, runtime) = {
473 let cmd = pkg.get_command(command_name).with_context(|| {
474 format!("Unable to get metadata for the \"{command_name}\" command")
475 })?;
476 let module = runtime.resolve_module_sync(
477 wasmer_wasix::runtime::ModuleInput::Command(Cow::Borrowed(cmd)),
478 None,
479 None,
480 )?;
481 let runtime = self.maybe_wrap_runtime_with_napi(&module, runtime)?;
482 (module, runtime)
483 };
484
485 let mut runner = self.build_wasi_runner(&runtime, true)?;
487 #[cfg(feature = "napi-v8")]
488 self.configure_wasi_runner_for_napi(&module, &mut runner);
489 Runner::run_command(&mut runner, command_name, pkg, runtime)
490 }
491
492 #[tracing::instrument(skip_all)]
493 fn execute_pure_wasm_module(&self, module: &Module) -> Result<(), Error> {
494 let mut store = self.rt.get_store()?;
497 let imports = Imports::default();
498 let instance = Instance::new(&mut store, module, &imports)
499 .context("Unable to instantiate the WebAssembly module")?;
500
501 let entry_function = match &self.invoke {
502 Some(entry) => {
503 instance.exports
504 .get_function(entry)
505 .with_context(|| format!("The module doesn't export a function named \"{entry}\""))?
506 },
507 None => {
508 instance.exports.get_function("_start")
509 .context("The module doesn't export a \"_start\" function. Either implement it or specify an entry function with --invoke")?
510 }
511 };
512
513 let result = invoke_function(&instance, &mut store, entry_function, &self.args)?;
514
515 match result {
516 Ok(return_values) => {
517 println!(
518 "{}",
519 return_values
520 .iter()
521 .map(|val| val.to_string())
522 .collect::<Vec<String>>()
523 .join(" ")
524 );
525 Ok(())
526 }
527 Err(err) => {
528 bail!("{}", err.display(&mut store));
529 }
530 }
531 }
532
533 fn build_wasi_runner(
534 &self,
535 runtime: &Arc<dyn Runtime + Send + Sync>,
536 is_wasix: bool,
537 ) -> Result<WasiRunner, anyhow::Error> {
538 let packages = self.load_injected_packages(runtime)?;
539
540 let mut runner = WasiRunner::new();
541
542 let (is_home_mapped, mapped_directories) = self.wasi.build_mapped_directories(is_wasix)?;
543
544 runner
545 .with_args(&self.args)
546 .with_injected_packages(packages)
547 .with_envs(self.wasi.env_vars.clone())
548 .with_mapped_host_commands(self.wasi.build_mapped_commands()?)
549 .with_mapped_directories(mapped_directories)
550 .with_home_mapped(is_home_mapped)
551 .with_forward_host_env(self.wasi.forward_host_env)
552 .with_capabilities(self.wasi.capabilities());
553
554 if let Some(cwd) = self.wasi.cwd.as_ref() {
555 if !cwd.starts_with("/") {
556 bail!("The argument to --cwd must be an absolute path");
557 }
558 runner.with_current_dir(cwd.clone());
559 }
560
561 if let Some(ref entry_function) = self.invoke {
562 runner.with_entry_function(entry_function);
563 }
564
565 #[cfg(feature = "journal")]
566 {
567 for trigger in self.wasi.snapshot_on.iter().cloned() {
568 runner.with_snapshot_trigger(trigger);
569 }
570 if self.wasi.snapshot_on.is_empty() && !self.wasi.writable_journals.is_empty() {
571 runner.with_default_snapshot_triggers();
572 }
573 if let Some(period) = self.wasi.snapshot_interval {
574 if self.wasi.writable_journals.is_empty() {
575 return Err(anyhow::format_err!(
576 "If you specify a snapshot interval then you must also specify a writable journal file"
577 ));
578 }
579 runner.with_snapshot_interval(Duration::from_millis(period));
580 }
581 if self.wasi.stop_after_snapshot {
582 runner.with_stop_running_after_snapshot(true);
583 }
584 let (r, w) = self.wasi.build_journals()?;
585 for journal in r {
586 runner.with_read_only_journal(journal);
587 }
588 for journal in w {
589 runner.with_writable_journal(journal);
590 }
591 runner.with_skip_stdio_during_bootstrap(self.wasi.skip_stdio_during_bootstrap);
592 }
593
594 Ok(runner)
595 }
596
597 #[tracing::instrument(skip_all)]
598 fn execute_wasi_module(
599 &self,
600 wasm_path: &Path,
601 module: Module,
602 module_hash: ModuleHash,
603 runtime: Arc<dyn Runtime + Send + Sync>,
604 ) -> Result<(), Error> {
605 let program_name = wasm_path.display().to_string();
606 let runtime = self.maybe_wrap_runtime_with_napi(&module, runtime)?;
607
608 let mut runner =
609 self.build_wasi_runner(&runtime, wasmer_wasix::is_wasix_module(&module))?;
610 self.configure_wasi_runner_for_napi(&module, &mut runner);
611 runner.run_wasm(
612 RuntimeOrEngine::Runtime(runtime),
613 &program_name,
614 module,
615 module_hash,
616 )
617 }
618
619 #[allow(unused_variables)]
620 fn maybe_save_coredump(&self, e: &Error) {
621 #[cfg(feature = "coredump")]
622 if let Some(coredump) = &self.coredump_on_trap
623 && let Err(e) = generate_coredump(e, self.input.to_string(), coredump)
624 {
625 tracing::warn!(
626 error = &*e as &dyn std::error::Error,
627 coredump_path=%coredump.display(),
628 "Unable to generate a coredump",
629 );
630 }
631 }
632
633 fn print_option_warnings(&self) {
634 if !self.wasi.mapped_dirs.is_empty() {
635 eprintln!(
636 "{}The `{}` option is deprecated and will be removed in the next major release. Please use `{}` instead.",
637 "warning: ".yellow(),
638 "--mapdir".yellow(),
639 "--volume".green()
640 );
641 }
642 if !self.wasi.pre_opened_directories.is_empty() {
643 eprintln!(
644 "{}The `{}` option is deprecated and will be removed in the next major release. Please use `{}` instead.",
645 "warning: ".yellow(),
646 "--dir".yellow(),
647 "--volume".green()
648 );
649 }
650 }
651}
652
653fn invoke_function(
654 instance: &Instance,
655 store: &mut Store,
656 func: &Function,
657 args: &[String],
658) -> anyhow::Result<Result<Box<[Value]>, RuntimeError>> {
659 let func_ty = func.ty(store);
660 let required_arguments = func_ty.params().len();
661 let provided_arguments = args.len();
662
663 anyhow::ensure!(
664 required_arguments == provided_arguments,
665 "Function expected {required_arguments} arguments, but received {provided_arguments}"
666 );
667
668 let invoke_args = args
669 .iter()
670 .zip(func_ty.params().iter())
671 .map(|(arg, param_type)| {
672 parse_value(arg, *param_type)
673 .with_context(|| format!("Unable to convert {arg:?} to {param_type:?}"))
674 })
675 .collect::<Result<Vec<_>, _>>()?;
676
677 Ok(func.call(store, &invoke_args))
678}
679
680fn parse_value(s: &str, ty: wasmer_types::Type) -> Result<Value, Error> {
681 let value = match ty {
682 Type::I32 => Value::I32(s.parse()?),
683 Type::I64 => Value::I64(s.parse()?),
684 Type::F32 => Value::F32(s.parse()?),
685 Type::F64 => Value::F64(s.parse()?),
686 Type::V128 => Value::V128(s.parse()?),
687 _ => bail!("There is no known conversion from {s:?} to {ty:?}"),
688 };
689 Ok(value)
690}
691
692#[cfg(feature = "coredump")]
693fn generate_coredump(err: &Error, source_name: String, coredump_path: &Path) -> Result<(), Error> {
694 let err: &wasmer::RuntimeError = match err.downcast_ref() {
695 Some(e) => e,
696 None => {
697 log::warn!("no runtime error found to generate coredump with");
698 return Ok(());
699 }
700 };
701
702 let mut coredump_builder =
703 wasm_coredump_builder::CoredumpBuilder::new().executable_name(&source_name);
704
705 let mut thread_builder = wasm_coredump_builder::ThreadBuilder::new().thread_name("main");
706
707 for frame in err.trace() {
708 let coredump_frame = wasm_coredump_builder::FrameBuilder::new()
709 .codeoffset(frame.func_offset() as u32)
710 .funcidx(frame.func_index())
711 .build();
712 thread_builder.add_frame(coredump_frame);
713 }
714
715 coredump_builder.add_thread(thread_builder.build());
716
717 let coredump = coredump_builder
718 .serialize()
719 .map_err(Error::msg)
720 .context("Coredump serializing failed")?;
721
722 std::fs::write(coredump_path, &coredump).with_context(|| {
723 format!(
724 "Unable to save the coredump to \"{}\"",
725 coredump_path.display()
726 )
727 })?;
728
729 Ok(())
730}
731
732fn exit_with_wasi_exit_code(result: Result<(), Error>) -> ! {
735 let exit_code = match result {
736 Ok(_) => 0,
737 Err(error) => {
738 match error.chain().find_map(get_exit_code) {
739 Some(exit_code) => exit_code.raw(),
740 None => {
741 eprintln!("{:?}", PrettyError::new(error));
742 1
744 }
745 }
746 }
747 };
748
749 std::io::stdout().flush().ok();
750 std::io::stderr().flush().ok();
751
752 std::process::exit(exit_code);
753}
754
755fn get_exit_code(
756 error: &(dyn std::error::Error + 'static),
757) -> Option<wasmer_wasix::types::wasi::ExitCode> {
758 if let Some(WasiError::Exit(exit_code)) = error.downcast_ref() {
759 return Some(*exit_code);
760 }
761 if let Some(error) = error.downcast_ref::<wasmer_wasix::WasiRuntimeError>() {
762 return error.as_exit_code();
763 }
764
765 None
766}