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