1use std::{
2 collections::{BTreeSet, HashMap},
3 ffi::OsString,
4 path::{Path, PathBuf},
5 str::FromStr,
6 sync::{Arc, mpsc::Sender},
7 time::Duration,
8};
9
10use anyhow::{Context, Result, bail};
11use bytes::Bytes;
12use clap::Parser;
13use itertools::Itertools;
14use tokio::runtime::Handle;
15use url::Url;
16use virtual_fs::{
17 ArcFileSystem, DeviceFile, FileSystem, MountFileSystem, OverlayFileSystem,
18 RootFileSystemBuilder,
19};
20use virtual_net::ruleset::Ruleset;
21use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value};
22use wasmer_config::package::PackageSource as PackageSpecifier;
23use wasmer_types::ModuleHash;
24#[cfg(feature = "journal")]
25use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
26use wasmer_wasix::{
27 PluggableRuntime, RewindState, Runtime, WasiEnv, WasiEnvBuilder, WasiError, WasiFunctionEnv,
28 WasiVersion,
29 bin_factory::BinaryPackage,
30 capabilities::Capabilities,
31 get_wasi_versions,
32 http::HttpClient,
33 journal::{CompactingLogFileJournal, DynJournal, DynReadableJournal},
34 os::{TtyBridge, tty_sys::SysTty},
35 rewind_ext,
36 runners::MAPPED_CURRENT_DIR_DEFAULT_PATH,
37 runners::{MappedCommand, MappedDirectory, MountedDirectory},
38 runtime::{
39 module_cache::{FileSystemCache, ModuleCache},
40 package_loader::{BuiltinPackageLoader, PackageLoader},
41 resolver::{
42 BackendSource, FileSystemSource, InMemorySource, LocalRegistrySource, MultiSource,
43 Source, WebSource,
44 },
45 task_manager::{
46 VirtualTaskManagerExt,
47 tokio::{RuntimeOrHandle, TokioTaskManager},
48 },
49 },
50 types::__WASI_STDIN_FILENO,
51 wasmer_wasix_types::wasi::Errno,
52};
53
54use crate::{
55 config::{UserRegistry, WasmerEnv},
56 utils::{
57 WAPM_SOURCE_CACHE_TIMEOUT, parse_envvar, parse_mapdir, parse_volume,
58 registry_query_cache_dir,
59 },
60};
61
62use super::{
63 CliPackageSource, ExecutableTarget,
64 capabilities::{self, PkgCapabilityCache},
65};
66
67#[derive(Debug, Parser, Clone, Default)]
68pub struct Wasi {
70 #[clap(
72 long = "volume",
73 name = "[HOST_DIR:]GUEST_DIR",
74 value_parser = parse_volume,
75 )]
76 pub(crate) volumes: Vec<MappedDirectory>,
77
78 #[clap(long = "dir", group = "wasi", hide = true)]
80 pub(crate) pre_opened_directories: Vec<PathBuf>,
81
82 #[clap(
84 long = "mapdir",
85 value_parser = parse_mapdir,
86 hide = true
87 )]
88 pub(crate) mapped_dirs: Vec<MappedDirectory>,
89
90 #[clap(long = "cwd")]
93 pub(crate) cwd: Option<PathBuf>,
94
95 #[clap(
97 long = "env",
98 name = "KEY=VALUE",
99 value_parser=parse_envvar,
100 )]
101 pub(crate) env_vars: Vec<(String, String)>,
102
103 #[clap(long, env)]
105 pub(crate) forward_host_env: bool,
106
107 #[clap(long = "use", name = "USE")]
109 pub(crate) uses: Vec<String>,
110
111 #[clap(long = "include-webc", name = "WEBC")]
117 pub(super) include_webcs: Vec<PathBuf>,
118
119 #[clap(long = "offline")]
122 pub(super) offline: bool,
123
124 #[clap(long = "map-command", name = "MAPCMD")]
126 pub(super) map_commands: Vec<String>,
127
128 #[clap(long = "net", require_equals = true)]
147 pub networking: Option<Option<String>>,
150
151 #[clap(long = "no-tty")]
153 pub no_tty: bool,
154
155 #[clap(
159 long = "enable-async-threads",
160 require_equals = true,
161 default_missing_value = "true",
162 num_args = 0..=1,
163 action = clap::ArgAction::Set
164 )]
165 pub enable_async_threads: Option<bool>,
166
167 #[clap(long = "enable-cpu-backoff")]
172 pub enable_cpu_backoff: Option<u64>,
173
174 #[cfg(feature = "journal")]
180 #[clap(long = "journal")]
181 pub read_only_journals: Vec<PathBuf>,
182
183 #[cfg(feature = "journal")]
193 #[clap(long = "journal-writable")]
194 pub writable_journals: Vec<PathBuf>,
195
196 #[cfg(feature = "journal")]
199 #[clap(long = "enable-compaction")]
200 pub enable_compaction: bool,
201
202 #[cfg(feature = "journal")]
204 #[clap(long = "without-compact-on-drop")]
205 pub without_compact_on_drop: bool,
206
207 #[cfg(feature = "journal")]
213 #[clap(long = "with-compact-on-growth", default_value = "0.15")]
214 pub with_compact_on_growth: f32,
215
216 #[cfg(feature = "journal")]
227 #[clap(long = "snapshot-on")]
228 pub snapshot_on: Vec<SnapshotTrigger>,
229
230 #[cfg(feature = "journal")]
234 #[clap(long = "snapshot-period")]
235 pub snapshot_interval: Option<u64>,
236
237 #[cfg(feature = "journal")]
240 #[clap(long = "stop-after-snapshot")]
241 pub stop_after_snapshot: bool,
242
243 #[cfg(feature = "journal")]
245 #[clap(long = "skip-journal-stdio")]
246 pub skip_stdio_during_bootstrap: bool,
247
248 #[clap(long)]
252 pub http_client: bool,
253
254 #[clap(long = "deny-multiple-wasi-versions")]
256 pub deny_multiple_wasi_versions: bool,
257
258 #[clap(long = "disable-cache")]
263 disable_cache: bool,
264}
265
266pub struct RunProperties {
267 pub ctx: WasiFunctionEnv,
268 pub path: PathBuf,
269 pub invoke: Option<String>,
270 pub args: Vec<String>,
271}
272
273fn utf8_env_part(part: OsString) -> Result<String> {
276 part.into_string()
277 .map_err(|part| anyhow::anyhow!("environment variable is not valid UTF-8: {part:?}"))
278}
279
280#[allow(dead_code)]
281impl Wasi {
282 pub fn map_dir(&mut self, alias: &str, target_on_disk: PathBuf) {
283 self.volumes.push(MappedDirectory {
284 guest: alias.to_string(),
285 host: target_on_disk,
286 });
287 }
288
289 pub fn set_env(&mut self, key: &str, value: &str) {
290 self.env_vars.push((key.to_string(), value.to_string()));
291 }
292
293 pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
295 get_wasi_versions(module, false)
300 }
301
302 pub fn has_wasi_imports(module: &Module) -> bool {
304 get_wasi_versions(module, false).is_some()
307 }
308
309 pub(crate) fn all_volumes(&self) -> Vec<MappedDirectory> {
310 self.volumes
311 .iter()
312 .cloned()
313 .chain(self.pre_opened_directories.iter().map(|d| MappedDirectory {
314 host: d.clone(),
315 guest: d.to_str().expect("must be a valid path string").to_string(),
316 }))
317 .chain(self.mapped_dirs.iter().cloned())
318 .collect_vec()
319 }
320
321 pub fn prepare(
322 &self,
323 module: &Module,
324 program_name: String,
325 args: Vec<String>,
326 rt: Arc<dyn Runtime + Send + Sync>,
327 ) -> Result<WasiEnvBuilder> {
328 let args = args.into_iter().map(|arg| arg.into_bytes());
329
330 let map_commands = self
331 .map_commands
332 .iter()
333 .map(|map| map.split_once('=').unwrap())
334 .map(|(a, b)| (a.to_string(), b.to_string()))
335 .collect::<HashMap<_, _>>();
336
337 let mut uses = Vec::new();
338 for name in &self.uses {
339 let specifier = PackageSpecifier::from_str(name)
340 .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
341 let pkg = {
342 let inner_rt = rt.clone();
343 rt.task_manager()
344 .spawn_and_block_on(async move {
345 BinaryPackage::from_registry(&specifier, &*inner_rt).await
346 })
347 .with_context(|| format!("Unable to load \"{name}\""))??
348 };
349 uses.push(pkg);
350 }
351
352 let mut builder = WasiEnv::builder(program_name)
353 .runtime(Arc::clone(&rt))
354 .args(args)
355 .envs(self.env_vars.clone())
356 .uses(uses)
357 .map_commands(map_commands);
358
359 let mut builder = {
360 let mount_fs = RootFileSystemBuilder::new()
361 .with_tty(Box::new(DeviceFile::new(__WASI_STDIN_FILENO)))
362 .build();
363 let (have_current_dir, mapped_dirs) = self.build_mapped_directories(false)?;
364 let mut root_layers: Vec<Arc<dyn FileSystem + Send + Sync>> = Vec::new();
365
366 for mapped in mapped_dirs {
367 let MountedDirectory { guest, fs } = MountedDirectory::from(mapped);
368 if guest == "/" {
369 root_layers.push(fs);
370 } else {
371 mount_fs.mount(&guest, Arc::new(fs))?;
372 }
373 }
374
375 if !root_layers.is_empty() {
376 let existing_root = mount_fs
377 .filesystem_at(Path::new("/"))
378 .expect("root fs builder should always mount /");
379 mount_fs.set_mount(
380 Path::new("/"),
381 Arc::new(OverlayFileSystem::new(
382 ArcFileSystem::new(existing_root),
383 root_layers,
384 )),
385 )?;
386 };
387
388 if let Some(cwd) = self.cwd.as_ref() {
389 if !cwd.starts_with("/") {
390 bail!("The argument to --cwd must be an absolute path");
391 }
392 builder = builder.current_dir(cwd.clone());
393 }
394
395 builder = builder
397 .mount_fs(mount_fs)
398 .preopen_dir(Path::new("/"))
399 .unwrap();
400
401 let dot_path = if have_current_dir {
402 PathBuf::from(MAPPED_CURRENT_DIR_DEFAULT_PATH)
403 } else {
404 PathBuf::from("/")
405 };
406
407 builder.add_preopen_build(|p| {
408 p.directory(&dot_path)
409 .alias(".")
410 .read(true)
411 .write(true)
412 .create(true)
413 })?;
414
415 builder
416 };
417
418 *builder.capabilities_mut() = self.capabilities();
419
420 #[cfg(feature = "journal")]
421 {
422 for trigger in self.snapshot_on.iter().cloned() {
423 builder.add_snapshot_trigger(trigger);
424 }
425 if let Some(interval) = self.snapshot_interval {
426 builder.with_snapshot_interval(std::time::Duration::from_millis(interval));
427 }
428 if self.stop_after_snapshot {
429 builder.with_stop_running_after_snapshot(true);
430 }
431 let (r, w) = self.build_journals()?;
432 for journal in r {
433 builder.add_read_only_journal(journal);
434 }
435 for journal in w {
436 builder.add_writable_journal(journal);
437 }
438 builder.with_skip_stdio_during_bootstrap(self.skip_stdio_during_bootstrap);
439 }
440
441 Ok(builder)
442 }
443
444 #[cfg(feature = "journal")]
445 #[allow(clippy::type_complexity)]
446 pub fn build_journals(
447 &self,
448 ) -> anyhow::Result<(Vec<Arc<DynReadableJournal>>, Vec<Arc<DynJournal>>)> {
449 let mut readable = Vec::new();
450 for journal in self.read_only_journals.clone() {
451 if matches!(std::fs::metadata(&journal), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
452 {
453 bail!("Read-only journal file does not exist: {journal:?}");
454 }
455
456 readable
457 .push(Arc::new(LogFileJournal::new_readonly(journal)?) as Arc<DynReadableJournal>);
458 }
459
460 let mut writable = Vec::new();
461 for journal in self.writable_journals.clone() {
462 if self.enable_compaction {
463 let mut journal = CompactingLogFileJournal::new(journal)?;
464 if !self.without_compact_on_drop {
465 journal = journal.with_compact_on_drop()
466 }
467 if self.with_compact_on_growth.is_normal() && self.with_compact_on_growth != 0f32 {
468 journal = journal.with_compact_on_factor_size(self.with_compact_on_growth);
469 }
470 writable.push(Arc::new(journal) as Arc<DynJournal>);
471 } else {
472 writable.push(Arc::new(LogFileJournal::new(journal)?));
473 }
474 }
475 Ok((readable, writable))
476 }
477
478 #[cfg(not(feature = "journal"))]
479 pub fn build_journals(&self) -> anyhow::Result<Vec<Arc<DynJournal>>> {
480 Ok(Vec::new())
481 }
482
483 pub fn build_mapped_directories(
484 &self,
485 is_wasix: bool,
486 ) -> Result<(bool, Vec<MappedDirectory>), anyhow::Error> {
487 let mut mapped_dirs = Vec::new();
488
489 let mut have_current_dir = false;
491 for MappedDirectory { host, guest } in &self.all_volumes() {
492 let resolved_host = host.canonicalize().with_context(|| {
493 format!(
494 "could not canonicalize path for argument '--volume {}:{}'",
495 host.display(),
496 guest,
497 )
498 })?;
499
500 if guest == "/" && is_wasix {
501 tracing::warn!(
504 "Mounting on the guest's virtual root with --volume <HOST_DIR>:/ breaks WASIX modules' filesystems"
505 );
506 }
507
508 let mapping = if guest == "." {
509 if have_current_dir {
510 bail!(
511 "Cannot pre-open the current directory twice: '--volume=.' must only be specified once"
512 );
513 }
514 have_current_dir = true;
515
516 let host = if host == Path::new(".") {
517 std::env::current_dir().context("could not determine current directory")?
518 } else {
519 host.clone()
520 };
521 MappedDirectory {
522 host: resolved_host,
523 guest: if is_wasix {
524 MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string()
525 } else {
526 "/".to_string()
527 },
528 }
529 } else {
530 MappedDirectory {
531 host: resolved_host,
532 guest: guest.clone(),
533 }
534 };
535 mapped_dirs.push(mapping);
536 }
537
538 Ok((have_current_dir, mapped_dirs))
539 }
540
541 pub fn build_mapped_commands(&self) -> Result<Vec<MappedCommand>, anyhow::Error> {
542 self.map_commands
543 .iter()
544 .map(|item| {
545 let (a, b) = item.split_once('=').with_context(|| {
546 format!(
547 "Invalid --map-command flag: expected <ALIAS>=<HOST_PATH>, got '{item}'"
548 )
549 })?;
550
551 let a = a.trim();
552 let b = b.trim();
553
554 if a.is_empty() {
555 bail!("Invalid --map-command flag - alias cannot be empty: '{item}'");
556 }
557 if b.is_empty() {
559 bail!("Invalid --map-command flag - host path cannot be empty: '{item}'");
560 }
561
562 Ok(MappedCommand {
563 alias: a.to_string(),
564 target: b.to_string(),
565 })
566 })
567 .collect::<Result<Vec<_>, anyhow::Error>>()
568 }
569
570 pub fn capabilities(&self) -> Capabilities {
571 let mut caps = Capabilities::default();
572
573 if self.http_client {
574 caps.http_client = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all();
575 }
576
577 if let Some(enable_async_threads) = self.enable_async_threads {
578 caps.threading.enable_asynchronous_threading = enable_async_threads;
579 }
580 caps.threading.enable_exponential_cpu_backoff =
581 self.enable_cpu_backoff.map(Duration::from_millis);
582
583 caps
584 }
585
586 pub fn prepare_runtime<I>(
587 &self,
588 engine: Engine,
589 env: &WasmerEnv,
590 pkg_cache_path: &Path,
591 rt_or_handle: I,
592 preferred_webc_version: webc::Version,
593 compiler_debug_dir_used: bool,
594 ) -> Result<impl Runtime + Send + Sync + use<I>>
595 where
596 I: Into<RuntimeOrHandle>,
597 {
598 let tokio_task_manager = Arc::new(TokioTaskManager::new(rt_or_handle.into()));
599 let mut rt = PluggableRuntime::new(tokio_task_manager.clone());
600
601 let has_networking = self.networking.is_some()
602 || capabilities::get_cached_capability(pkg_cache_path)
603 .ok()
604 .is_some_and(|v| v.enable_networking);
605
606 let ruleset = self
607 .networking
608 .clone()
609 .flatten()
610 .map(|ruleset| Ruleset::from_str(&ruleset))
611 .transpose()?;
612
613 let network = if let Some(ruleset) = ruleset {
614 virtual_net::host::LocalNetworking::with_ruleset(ruleset)
615 } else {
616 virtual_net::host::LocalNetworking::default()
617 };
618
619 if has_networking {
620 rt.set_networking_implementation(network);
621 } else {
622 let net = super::capabilities::net::AskingNetworking::new(
623 pkg_cache_path.to_path_buf(),
624 Arc::new(network),
625 );
626
627 rt.set_networking_implementation(net);
628 }
629
630 #[cfg(feature = "journal")]
631 {
632 let (r, w) = self.build_journals()?;
633 for journal in r {
634 rt.add_read_only_journal(journal);
635 }
636 for journal in w {
637 rt.add_writable_journal(journal);
638 }
639 }
640
641 if !self.no_tty {
642 let tty = Arc::new(SysTty);
643 tty.reset();
644 rt.set_tty(tty);
645 }
646
647 let client =
648 wasmer_wasix::http::default_http_client().context("No HTTP client available")?;
649 let client = Arc::new(client);
650
651 let package_loader = self
652 .prepare_package_loader(env, client.clone())
653 .context("Unable to prepare the package loader")?;
654
655 let registry = self.prepare_source(env, client, preferred_webc_version)?;
656
657 if !self.disable_cache && !compiler_debug_dir_used {
658 let cache_dir = env.cache_dir().join("compiled");
659 let module_cache = wasmer_wasix::runtime::module_cache::in_memory()
660 .with_fallback(FileSystemCache::new(cache_dir, tokio_task_manager));
661 rt.set_module_cache(module_cache);
662 }
663
664 rt.set_package_loader(package_loader)
665 .set_source(registry)
666 .set_engine(engine);
667
668 Ok(rt)
669 }
670
671 pub fn instantiate(
673 &self,
674 module: &Module,
675 module_hash: ModuleHash,
676 program_name: String,
677 args: Vec<String>,
678 runtime: Arc<dyn Runtime + Send + Sync>,
679 store: &mut Store,
680 ) -> Result<(WasiFunctionEnv, Instance)> {
681 let builder = self.prepare(module, program_name, args, runtime)?;
682 let (instance, wasi_env) = builder.instantiate_ext(module.clone(), module_hash, store)?;
683
684 Ok((wasi_env, instance))
685 }
686
687 pub fn for_binfmt_interpreter() -> Result<Self> {
688 let dir = std::env::var_os("WASMER_BINFMT_MISC_PREOPEN")
689 .map(Into::into)
690 .unwrap_or_else(|| PathBuf::from("."));
691 Ok(Self {
692 deny_multiple_wasi_versions: true,
693 env_vars: std::env::vars_os()
694 .map(|(name, value)| Ok((utf8_env_part(name)?, utf8_env_part(value)?)))
695 .collect::<Result<_>>()?,
696 volumes: vec![MappedDirectory {
697 host: dir.clone(),
698 guest: dir
699 .to_str()
700 .expect("dir must be a valid string")
701 .to_string(),
702 }],
703 ..Self::default()
704 })
705 }
706
707 fn prepare_package_loader(
708 &self,
709 env: &WasmerEnv,
710 client: Arc<dyn HttpClient + Send + Sync>,
711 ) -> Result<BuiltinPackageLoader> {
712 let checkout_dir = env.cache_dir().join("checkouts");
713 let tokens = tokens_by_authority(env)?;
714
715 let loader = BuiltinPackageLoader::new()
716 .with_cache_dir(checkout_dir)
717 .with_shared_http_client(client)
718 .with_tokens(tokens);
719
720 Ok(loader)
721 }
722
723 fn prepare_source(
724 &self,
725 env: &WasmerEnv,
726 client: Arc<dyn HttpClient + Send + Sync>,
727 preferred_webc_version: webc::Version,
728 ) -> Result<MultiSource> {
729 let mut source = MultiSource::default();
730
731 let mut preloaded = InMemorySource::new();
735 for path in &self.include_webcs {
736 if path.is_dir() {
737 source.add_source(LocalRegistrySource::new(path)?);
738 } else {
739 preloaded
740 .add_webc(path)
741 .with_context(|| format!("Unable to load \"{}\"", path.display()))?;
742 }
743 }
744 source.add_source(preloaded);
745
746 if !self.offline {
750 let graphql_endpoint = self.graphql_endpoint(env)?;
751 let cache_dir = registry_query_cache_dir(env.cache_dir(), &graphql_endpoint);
752 let mut wapm_source = BackendSource::new(graphql_endpoint, Arc::clone(&client))
753 .with_local_cache(cache_dir, WAPM_SOURCE_CACHE_TIMEOUT)
754 .with_preferred_webc_version(preferred_webc_version);
755 if let Some(token) = env
756 .config()?
757 .registry
758 .get_login_token_for_registry(wapm_source.registry_endpoint().as_str())
759 {
760 wapm_source = wapm_source.with_auth_token(token);
761 }
762 source.add_source(wapm_source);
763
764 let cache_dir = env.cache_dir().join("downloads");
765 source.add_source(WebSource::new(cache_dir, client));
766 }
767
768 source.add_source(FileSystemSource::default());
769
770 Ok(source)
771 }
772
773 fn graphql_endpoint(&self, env: &WasmerEnv) -> Result<Url> {
774 if let Ok(endpoint) = env.registry_endpoint() {
775 return Ok(endpoint);
776 }
777
778 let config = env.config()?;
779 let graphql_endpoint = config.registry.get_graphql_url();
780 let graphql_endpoint = graphql_endpoint
781 .parse()
782 .with_context(|| format!("Unable to parse \"{graphql_endpoint}\" as a URL"))?;
783
784 Ok(graphql_endpoint)
785 }
786}
787
788fn parse_registry(r: &str) -> Result<Url> {
789 UserRegistry::from(r).graphql_endpoint()
790}
791
792fn tokens_by_authority(env: &WasmerEnv) -> Result<HashMap<String, String>> {
793 let mut tokens = HashMap::new();
794 let config = env.config()?;
795
796 for credentials in config.registry.tokens {
797 if let Ok(url) = Url::parse(&credentials.registry)
798 && url.has_authority()
799 {
800 tokens.insert(url.authority().to_string(), credentials.token);
801 }
802 }
803
804 if let (Ok(current_registry), Some(token)) = (env.registry_endpoint(), env.token())
805 && current_registry.has_authority()
806 {
807 tokens.insert(current_registry.authority().to_string(), token);
808 }
809
810 let mut frontend_tokens = HashMap::new();
823 for (hostname, token) in &tokens {
824 if let Some(frontend_url) = hostname.strip_prefix("registry.")
825 && !tokens.contains_key(frontend_url)
826 {
827 frontend_tokens.insert(frontend_url.to_string(), token.clone());
828 }
829 }
830 tokens.extend(frontend_tokens);
831
832 Ok(tokens)
833}