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