Skip to main content

wasmer_wasix/state/
env.rs

1#[cfg(feature = "journal")]
2use crate::journal::{DynJournal, JournalEffector, SnapshotTrigger};
3use crate::{
4    Runtime, VirtualTaskManager, WasiControlPlane, WasiEnvBuilder, WasiError, WasiFunctionEnv,
5    WasiResult, WasiRuntimeError, WasiStateCreationError, WasiThreadError, WasiVFork,
6    bin_factory::{BinFactory, BinaryPackage, BinaryPackageCommand},
7    capabilities::Capabilities,
8    fs::{WasiFsRoot, WasiInodes},
9    import_object_for_all_wasi_versions,
10    os::{
11        TtyBridge,
12        task::{
13            control_plane::ControlPlaneError,
14            process::{WasiProcess, WasiProcessId},
15            thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId},
16        },
17    },
18    state::PreparedInstanceGroupData,
19    syscalls::platform_clock_time_get,
20};
21use futures::future::BoxFuture;
22use rand::RngExt;
23use std::{
24    collections::{HashMap, HashSet},
25    ops::Deref,
26    path::{Path, PathBuf},
27    str,
28    sync::Arc,
29    time::Duration,
30};
31use virtual_fs::{FileSystem, FsError, VirtualFile};
32use virtual_mio::block_on;
33use virtual_net::DynVirtualNetworking;
34use wasmer::{
35    AsStoreMut, AsStoreRef, ExportError, FunctionEnvMut, Instance, Memory, MemoryType, MemoryView,
36    Module,
37};
38use wasmer_config::package::PackageSource;
39use wasmer_types::ModuleHash;
40use wasmer_wasix_types::{
41    types::Signal,
42    wasi::{Errno, ExitCode, Snapshot0Clockid},
43    wasix::ThreadStartType,
44};
45use webc::metadata::annotations::Wasi;
46
47pub use super::handles::*;
48use super::{Linker, WasiState, context_switching::ContextSwitchingEnvironment};
49
50fn add_command_env_defaults(environment: &mut Vec<Vec<u8>>, defaults: Vec<String>) {
51    let mut names = environment
52        .iter()
53        .filter_map(|entry| {
54            entry
55                .iter()
56                .position(|byte| *byte == b'=')
57                .map(|separator| entry[..separator].to_vec())
58        })
59        .collect::<HashSet<_>>();
60
61    for default in defaults {
62        let Some((key, value)) = default.split_once('=') else {
63            continue;
64        };
65        if names.insert(key.as_bytes().to_vec()) {
66            environment.push([key.as_bytes(), b"=", value.as_bytes()].concat());
67        }
68    }
69}
70
71async fn write_readonly_buffer_to_fs(
72    fs: &WasiFsRoot,
73    path: &Path,
74    contents: &shared_buffer::OwnedBuffer,
75) -> Result<(), FsError> {
76    if let Some(parent) = path.parent() {
77        virtual_fs::create_dir_all(fs, parent)?;
78    }
79
80    if let Some(root_fs) = fs.writable_root() {
81        return root_fs
82            .new_open_options_ext()
83            .insert_ro_file(path, contents.clone());
84    }
85
86    let mut file = fs
87        .new_open_options()
88        .create(true)
89        .truncate(true)
90        .write(true)
91        .open(path)?;
92    file.copy_from_owned_buffer(contents)
93        .await
94        .map_err(virtual_fs::FsError::from)
95}
96
97/// Data required to construct a [`WasiEnv`].
98#[derive(Debug)]
99pub struct WasiEnvInit {
100    pub(crate) state: WasiState,
101    pub runtime: Arc<dyn Runtime + Send + Sync>,
102    pub tty: Option<Arc<dyn TtyBridge + Send + Sync>>,
103    pub webc_dependencies: Vec<BinaryPackage>,
104    pub mapped_commands: HashMap<String, PathBuf>,
105    pub bin_factory: BinFactory,
106    pub capabilities: Capabilities,
107
108    pub control_plane: WasiControlPlane,
109    pub memory_ty: Option<MemoryType>,
110    pub process: Option<WasiProcess>,
111    pub thread: Option<WasiThreadHandle>,
112
113    /// Whether to call the `_initialize` function in the WASI module.
114    /// Will be true for regular new instances, but false for threads.
115    pub call_initialize: bool,
116
117    /// Indicates if the calling environment is capable of deep sleeping
118    pub can_deep_sleep: bool,
119
120    /// Indicates if extra tracing should be output
121    pub extra_tracing: bool,
122
123    /// Indicates triggers that will cause a snapshot to be taken
124    #[cfg(feature = "journal")]
125    pub snapshot_on: Vec<SnapshotTrigger>,
126
127    /// Stop running after the first snapshot is taken
128    #[cfg(feature = "journal")]
129    pub stop_running_after_snapshot: bool,
130
131    /// Skip writes to stdout and stderr when bootstrapping from a journal
132    pub skip_stdio_during_bootstrap: bool,
133}
134
135impl WasiEnvInit {
136    pub fn duplicate(&self) -> Self {
137        let inodes = WasiInodes::new();
138
139        // TODO: preserve preopens?
140        let fs =
141            crate::fs::WasiFs::new_with_preopen(&inodes, &[], &[], self.state.fs.root_fs.clone())
142                .unwrap();
143
144        Self {
145            state: WasiState {
146                secret: rand::rng().random::<[u8; 32]>(),
147                inodes,
148                fs,
149                futexs: Default::default(),
150                clock_offset: std::sync::Mutex::new(
151                    self.state.clock_offset.lock().unwrap().clone(),
152                ),
153                args: std::sync::Mutex::new(self.state.args.lock().unwrap().clone()),
154                envs: std::sync::Mutex::new(self.state.envs.lock().unwrap().deref().clone()),
155                signals: std::sync::Mutex::new(self.state.signals.lock().unwrap().deref().clone()),
156                signal_handler_registered: std::sync::atomic::AtomicBool::new(false),
157                preopen: self.state.preopen.clone(),
158            },
159            runtime: self.runtime.clone(),
160            tty: self.tty.clone(),
161            webc_dependencies: self.webc_dependencies.clone(),
162            mapped_commands: self.mapped_commands.clone(),
163            bin_factory: self.bin_factory.clone(),
164            capabilities: self.capabilities.clone(),
165            control_plane: self.control_plane.clone(),
166            memory_ty: None,
167            process: None,
168            thread: None,
169            call_initialize: self.call_initialize,
170            can_deep_sleep: self.can_deep_sleep,
171            extra_tracing: false,
172            #[cfg(feature = "journal")]
173            snapshot_on: self.snapshot_on.clone(),
174            #[cfg(feature = "journal")]
175            stop_running_after_snapshot: self.stop_running_after_snapshot,
176            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
177        }
178    }
179}
180
181/// The environment provided to the WASI imports.
182pub struct WasiEnv {
183    pub control_plane: WasiControlPlane,
184    /// Represents the process this environment is attached to
185    pub process: WasiProcess,
186    /// Represents the thread this environment is attached to
187    pub thread: WasiThread,
188    /// Represents the layout of the memory
189    pub layout: WasiMemoryLayout,
190    /// Represents a fork of the process that is currently in play
191    pub vfork: Option<WasiVFork>,
192    /// Seed used to rotate around the events returned by `poll_oneoff`
193    pub poll_seed: u64,
194    /// Shared state of the WASI system. Manages all the data that the
195    /// executing WASI program can see.
196    pub(crate) state: Arc<WasiState>,
197    /// Binary factory attached to this environment
198    pub bin_factory: BinFactory,
199    /// List of the handles that are owned by this context
200    /// (this can be used to ensure that threads own themselves or others)
201    pub owned_handles: Vec<WasiThreadHandle>,
202    /// Implementation of the WASI runtime.
203    pub runtime: Arc<dyn Runtime + Send + Sync + 'static>,
204    /// Terminal state scoped to this process tree, overriding the runtime default.
205    pub tty: Option<Arc<dyn TtyBridge + Send + Sync + 'static>>,
206
207    pub capabilities: Capabilities,
208
209    /// Is this environment capable and setup for deep sleeping
210    pub enable_deep_sleep: bool,
211
212    /// Enables the snap shotting functionality
213    pub enable_journal: bool,
214
215    /// Enables an exponential backoff of the process CPU usage when there
216    /// are no active run tokens (when set holds the maximum amount of
217    /// time that it will pause the CPU)
218    pub enable_exponential_cpu_backoff: Option<Duration>,
219
220    /// Flag that indicates if the environment is currently replaying the journal
221    /// (and hence it should not record new events)
222    pub replaying_journal: bool,
223
224    /// Should stdio be skipped when bootstrapping this module from an existing journal?
225    pub skip_stdio_during_bootstrap: bool,
226
227    /// Flag that indicates the cleanup of the environment is to be disabled
228    /// (this is normally used so that the instance can be reused later on)
229    pub(crate) disable_fs_cleanup: bool,
230
231    /// Inner functions and references that are loaded before the environment starts
232    /// (inner is not safe to send between threads and so it is private and will
233    ///  not be cloned when `WasiEnv` is cloned)
234    /// TODO: We should move this outside of `WasiEnv` with some refactoring
235    inner: WasiInstanceHandlesPointer,
236
237    /// Tracks the active contexts of the WASIX context switching API
238    ///
239    /// This is `None` when the main function was not launched with context switching
240    ///
241    /// Should probably only be set by [`ContextSwitchingContext::run_main_context`]
242    pub(crate) context_switching_environment: Option<ContextSwitchingEnvironment>,
243}
244
245impl std::fmt::Debug for WasiEnv {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        write!(f, "env(pid={}, tid={})", self.pid().raw(), self.tid().raw())
248    }
249}
250
251impl Clone for WasiEnv {
252    fn clone(&self) -> Self {
253        Self {
254            control_plane: self.control_plane.clone(),
255            process: self.process.clone(),
256            poll_seed: self.poll_seed,
257            thread: self.thread.clone(),
258            layout: self.layout.clone(),
259            vfork: self.vfork.clone(),
260            state: self.state.clone(),
261            bin_factory: self.bin_factory.clone(),
262            inner: Default::default(),
263            owned_handles: self.owned_handles.clone(),
264            runtime: self.runtime.clone(),
265            tty: self.tty.clone(),
266            capabilities: self.capabilities.clone(),
267            enable_deep_sleep: self.enable_deep_sleep,
268            enable_journal: self.enable_journal,
269            enable_exponential_cpu_backoff: self.enable_exponential_cpu_backoff,
270            replaying_journal: self.replaying_journal,
271            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
272            disable_fs_cleanup: self.disable_fs_cleanup,
273            context_switching_environment: None,
274        }
275    }
276}
277
278impl WasiEnv {
279    /// Construct a new [`WasiEnvBuilder`] that allows customizing an environment.
280    pub fn builder(program_name: impl Into<String>) -> WasiEnvBuilder {
281        WasiEnvBuilder::new(program_name)
282    }
283
284    /// Forking the WasiState is used when either fork or vfork is called
285    pub fn fork(&self) -> Result<(Self, WasiThreadHandle), ControlPlaneError> {
286        let process = self.control_plane.new_process(self.process.module_hash)?;
287        let handle = process.new_thread(self.layout.clone(), ThreadStartType::MainThread)?;
288
289        let thread = handle.as_thread();
290        thread.copy_stack_from(&self.thread);
291
292        let state = Arc::new(self.state.fork());
293
294        let bin_factory = self.bin_factory.clone();
295
296        let new_env = Self {
297            control_plane: self.control_plane.clone(),
298            process,
299            thread,
300            layout: self.layout.clone(),
301            vfork: None,
302            poll_seed: 0,
303            bin_factory,
304            state,
305            inner: Default::default(),
306            owned_handles: Vec::new(),
307            runtime: self.runtime.clone(),
308            tty: self.tty.clone(),
309            capabilities: self.capabilities.clone(),
310            enable_deep_sleep: self.enable_deep_sleep,
311            enable_journal: self.enable_journal,
312            enable_exponential_cpu_backoff: self.enable_exponential_cpu_backoff,
313            replaying_journal: false,
314            skip_stdio_during_bootstrap: self.skip_stdio_during_bootstrap,
315            disable_fs_cleanup: self.disable_fs_cleanup,
316            context_switching_environment: None,
317        };
318        Ok((new_env, handle))
319    }
320
321    pub fn pid(&self) -> WasiProcessId {
322        self.process.pid()
323    }
324
325    pub fn tid(&self) -> WasiThreadId {
326        self.thread.tid()
327    }
328
329    /// Returns true if this WASM process will need and try to use
330    /// asyncify while its running which normally means.
331    pub fn will_use_asyncify(&self) -> bool {
332        self.inner()
333            .static_module_instance_handles()
334            .map(|handles| self.enable_deep_sleep || handles.has_stack_checkpoint)
335            .unwrap_or(false)
336    }
337
338    /// Re-initializes this environment so that it can be executed again
339    pub fn reinit(&mut self) -> Result<(), WasiStateCreationError> {
340        // If the cleanup logic is enabled then we need to rebuild the
341        // file descriptors which would have been destroyed when the
342        // main thread exited
343        if !self.disable_fs_cleanup {
344            // First we clear any open files as the descriptors would
345            // otherwise clash
346            if let Ok(mut map) = self.state.fs.fd_map.write() {
347                map.clear();
348            }
349            self.state.fs.preopen_fds.write().unwrap().clear();
350            *self.state.fs.current_dir.lock().unwrap() = "/".to_string();
351
352            // We need to rebuild the basic file descriptors
353            self.state.fs.create_stdin(&self.state.inodes);
354            self.state.fs.create_stdout(&self.state.inodes);
355            self.state.fs.create_stderr(&self.state.inodes);
356            self.state
357                .fs
358                .create_rootfd()
359                .map_err(WasiStateCreationError::WasiFsSetupError)?;
360            self.state
361                .fs
362                .create_preopens(&self.state.inodes, true)
363                .map_err(WasiStateCreationError::WasiFsSetupError)?;
364        }
365
366        // The process and thread state need to be reset
367        self.process = WasiProcess::new(
368            self.process.pid,
369            self.process.module_hash,
370            self.process.compute.clone(),
371        );
372        self.thread = WasiThread::new(
373            self.thread.pid(),
374            self.thread.tid(),
375            self.thread.is_main(),
376            self.process.finished.clone(),
377            self.process.compute.must_upgrade().register_task()?,
378            self.thread.memory_layout().clone(),
379            self.thread.thread_start_type(),
380        );
381
382        Ok(())
383    }
384
385    /// Returns true if this module is capable of deep sleep
386    /// (needs asyncify to unwind and rewind)
387    ///
388    /// # Safety
389    ///
390    /// This function should only be called from within a syscall
391    /// as it accessed objects that are a thread local (functions)
392    pub unsafe fn capable_of_deep_sleep(&self) -> bool {
393        self.deep_sleep_capability_requested() && self.deep_sleep_supported_by_module()
394    }
395
396    pub(crate) fn refresh_deep_sleep_capability(&mut self) {
397        self.enable_deep_sleep = if cfg!(feature = "js") {
398            false
399        } else {
400            self.deep_sleep_capability_requested() && self.deep_sleep_supported_by_module()
401        };
402    }
403
404    fn deep_sleep_capability_requested(&self) -> bool {
405        self.capabilities.threading.enable_deep_sleep
406    }
407
408    fn deep_sleep_supported_by_module(&self) -> bool {
409        self.try_inner()
410            .map(|handles| {
411                handles
412                    .static_module_instance_handles()
413                    .map(|handles| {
414                        handles.asyncify_get_state.is_some()
415                            && handles.asyncify_start_rewind.is_some()
416                            && handles.asyncify_start_unwind.is_some()
417                    })
418                    .unwrap_or(false)
419            })
420            .unwrap_or(false)
421    }
422
423    /// Returns true if this thread can go into a deep sleep
424    pub fn layout(&self) -> &WasiMemoryLayout {
425        &self.layout
426    }
427
428    #[allow(clippy::result_large_err)]
429    pub(crate) fn from_init(
430        init: WasiEnvInit,
431        module_hash: ModuleHash,
432    ) -> Result<Self, WasiRuntimeError> {
433        let process = if let Some(p) = init.process {
434            p
435        } else {
436            init.control_plane.new_process(module_hash)?
437        };
438
439        #[cfg(feature = "journal")]
440        {
441            let mut guard = process.inner.0.lock().unwrap();
442            guard.snapshot_on = init.snapshot_on.into_iter().collect();
443            guard.stop_running_after_checkpoint = init.stop_running_after_snapshot;
444        }
445
446        let layout = WasiMemoryLayout::default();
447        let thread = if let Some(t) = init.thread {
448            t
449        } else {
450            process.new_thread(layout.clone(), ThreadStartType::MainThread)?
451        };
452
453        let mut env = Self {
454            control_plane: init.control_plane,
455            process,
456            thread: thread.as_thread(),
457            layout,
458            vfork: None,
459            poll_seed: 0,
460            state: Arc::new(init.state),
461            inner: Default::default(),
462            owned_handles: Vec::new(),
463            #[cfg(feature = "journal")]
464            enable_journal: init.runtime.active_journal().is_some(),
465            #[cfg(not(feature = "journal"))]
466            enable_journal: false,
467            replaying_journal: false,
468            skip_stdio_during_bootstrap: init.skip_stdio_during_bootstrap,
469            enable_deep_sleep: false,
470            enable_exponential_cpu_backoff: init
471                .capabilities
472                .threading
473                .enable_exponential_cpu_backoff,
474            runtime: init.runtime,
475            tty: init.tty,
476            bin_factory: init.bin_factory,
477            capabilities: init.capabilities,
478            disable_fs_cleanup: false,
479            context_switching_environment: None,
480        };
481        env.owned_handles.push(thread);
482
483        // TODO: should not be here - should be callers responsibility!
484        for pkg in &init.webc_dependencies {
485            env.use_package(pkg)?;
486        }
487
488        #[cfg(feature = "sys")]
489        env.map_commands(init.mapped_commands.clone())?;
490
491        Ok(env)
492    }
493
494    // FIXME: use custom error type
495    #[allow(clippy::result_large_err)]
496    pub(crate) fn instantiate(
497        self,
498        module: Module,
499        store: &mut impl AsStoreMut,
500        memory: Option<Memory>,
501        update_layout: bool,
502        call_initialize: bool,
503        linker_instance_group_data: Option<PreparedInstanceGroupData>,
504    ) -> Result<(Instance, WasiFunctionEnv), WasiThreadError> {
505        let pid = self.process.pid();
506
507        let mut store = store.as_store_mut();
508        let engine = self.runtime().engine();
509        let mut func_env = WasiFunctionEnv::new(&mut store, self);
510
511        let is_dl = super::linker::is_dynamically_linked(&module);
512        if is_dl {
513            let linker = match linker_instance_group_data {
514                Some(instance_group_data) => {
515                    Linker::create_instance_group(instance_group_data, &mut store, &mut func_env)
516                }
517                None => {
518                    // FIXME: should we be storing envs as raw byte arrays?
519                    let ld_library_path_owned;
520                    let ld_library_path = {
521                        let envs = func_env.data(&store).state.envs.lock().unwrap();
522                        ld_library_path_owned = match envs
523                            .iter()
524                            .find_map(|env| env.strip_prefix(b"LD_LIBRARY_PATH="))
525                        {
526                            Some(path) => path
527                                .split(|b| *b == b':')
528                                .filter_map(|p| str::from_utf8(p).ok())
529                                .map(PathBuf::from)
530                                .collect::<Vec<_>>(),
531                            None => vec![],
532                        };
533                        ld_library_path_owned
534                            .iter()
535                            .map(AsRef::as_ref)
536                            .collect::<Vec<_>>()
537                    };
538
539                    // TODO: make stack size configurable
540                    Linker::new(
541                        engine,
542                        &module,
543                        &mut store,
544                        memory,
545                        &mut func_env,
546                        8 * 1024 * 1024,
547                        &ld_library_path,
548                    )
549                }
550            };
551
552            match linker {
553                Ok((_, linked_module)) => {
554                    return Ok((linked_module.instance, func_env));
555                }
556                Err(e) => {
557                    tracing::error!(
558                        %pid,
559                        error = &e as &dyn std::error::Error,
560                        "Failed to link DL main module",
561                    );
562                    func_env
563                        .data(&store)
564                        .blocking_on_exit(Some(Errno::Noexec.into()));
565                    return Err(WasiThreadError::LinkError(Arc::new(e)));
566                }
567            }
568        }
569
570        // Let's instantiate the module with the imports.
571        let mut import_object =
572            import_object_for_all_wasi_versions(&module, &mut store, &func_env.env);
573        if let Some(memory) = memory.clone() {
574            import_object.define("env", "memory", memory);
575        }
576        let runtime = func_env.data(&store).runtime.clone();
577        let instantiation_state = runtime
578            .prepare_imports(&module, &mut store, &mut import_object)
579            .map_err(|err| WasiThreadError::ImportPreparationFailed(Arc::new(err)))?;
580
581        let imported_memory = import_object
582            .get_export("env", "memory")
583            .and_then(|ext| match ext {
584                wasmer::Extern::Memory(memory) => Some(memory),
585                _ => None,
586            });
587
588        // Construct the instance.
589        let instance = match Instance::new(&mut store, &module, &import_object) {
590            Ok(a) => a,
591            Err(err) => {
592                tracing::error!(
593                    %pid,
594                    error = &err as &dyn std::error::Error,
595                    "Instantiation failed",
596                );
597                func_env
598                    .data(&store)
599                    .blocking_on_exit(Some(Errno::Noexec.into()));
600                return Err(WasiThreadError::InstanceCreateFailed(Box::new(err)));
601            }
602        };
603
604        runtime
605            .configure_new_instance(
606                &module,
607                &mut store,
608                &instance,
609                imported_memory.as_ref(),
610                instantiation_state,
611            )
612            .map_err(|err| WasiThreadError::InstanceConfigurationFailed(Arc::new(err)))?;
613
614        let handles = match imported_memory {
615            Some(memory) => WasiModuleTreeHandles::Static(WasiModuleInstanceHandles::new(
616                memory,
617                &store,
618                instance.clone(),
619                None,
620            )),
621            None => {
622                let exported_memory = instance
623                    .exports
624                    .iter()
625                    .filter_map(|(_, export)| {
626                        if let wasmer::Extern::Memory(memory) = export {
627                            Some(memory.clone())
628                        } else {
629                            None
630                        }
631                    })
632                    .next()
633                    .ok_or(WasiThreadError::ExportError(ExportError::Missing(
634                        "No imported or exported memory found".to_owned(),
635                    )))?;
636                WasiModuleTreeHandles::Static(WasiModuleInstanceHandles::new(
637                    exported_memory,
638                    &store,
639                    instance.clone(),
640                    None,
641                ))
642            }
643        };
644
645        // Initialize the WASI environment
646        if let Err(err) = func_env.initialize_handles_and_layout(
647            &mut store,
648            instance.clone(),
649            handles,
650            None,
651            update_layout,
652        ) {
653            tracing::error!(
654                %pid,
655                error = &err as &dyn std::error::Error,
656                "Initialization failed",
657            );
658            func_env
659                .data(&store)
660                .blocking_on_exit(Some(Errno::Noexec.into()));
661            return Err(WasiThreadError::ExportError(err));
662        }
663
664        // If this module exports an _initialize function, run that first.
665        if call_initialize && let Ok(initialize) = instance.exports.get_function("_initialize") {
666            let initialize_result = initialize.call(&mut store, &[]);
667            if let Err(err) = initialize_result {
668                func_env
669                    .data(&store)
670                    .blocking_on_exit(Some(Errno::Noexec.into()));
671                return Err(WasiThreadError::InitFailed(Arc::new(anyhow::Error::from(
672                    err,
673                ))));
674            }
675        }
676
677        Ok((instance, func_env))
678    }
679
680    /// Returns a copy of the current runtime implementation for this environment
681    pub fn runtime(&self) -> &(dyn Runtime + Send + Sync) {
682        self.runtime.deref()
683    }
684
685    /// Returns the process-tree terminal, falling back to the runtime default.
686    pub fn tty(&self) -> Option<&(dyn TtyBridge + Send + Sync)> {
687        self.tty.as_deref().or_else(|| self.runtime.tty())
688    }
689
690    /// Returns a copy of the current tasks implementation for this environment
691    pub fn tasks(&self) -> &Arc<dyn VirtualTaskManager> {
692        self.runtime.task_manager()
693    }
694
695    pub fn fs_root(&self) -> &WasiFsRoot {
696        &self.state.fs.root_fs
697    }
698
699    /// Overrides the runtime implementation for this environment
700    pub fn set_runtime<R>(&mut self, runtime: R)
701    where
702        R: Runtime + Send + Sync + 'static,
703    {
704        self.runtime = Arc::new(runtime);
705    }
706
707    /// Returns the number of active threads
708    pub fn active_threads(&self) -> u32 {
709        self.process.active_threads()
710    }
711
712    /// Called by most (if not all) syscalls to process pending operations that are
713    /// cross-cutting, such as signals, thread/process exit, DL operations, etc.
714    pub fn do_pending_operations(ctx: &mut FunctionEnvMut<'_, Self>) -> Result<(), WasiError> {
715        Self::do_pending_link_operations(ctx, true)?;
716        _ = Self::process_signals_and_exit(ctx)?;
717        Ok(())
718    }
719
720    pub fn do_pending_link_operations(
721        ctx: &mut FunctionEnvMut<'_, Self>,
722        fast: bool,
723    ) -> Result<(), WasiError> {
724        if let Some(linker) = ctx.data().inner().linker().cloned()
725            && let Err(e) = linker.do_pending_link_operations(ctx, fast)
726        {
727            tracing::warn!(err = ?e, "Failed to process pending link operations");
728            return Err(WasiError::Exit(Errno::Noexec.into()));
729        }
730        Ok(())
731    }
732
733    /// Processes any signals that are batched up or any forced exit codes
734    pub fn process_signals_and_exit(ctx: &mut FunctionEnvMut<'_, Self>) -> WasiResult<bool> {
735        // If a signal handler has never been set then we need to handle signals
736        // differently
737        let env = ctx.data();
738        let env_inner = env
739            .try_inner()
740            .ok_or_else(|| WasiError::Exit(Errno::Fault.into()))?;
741        let inner = env_inner.main_module_instance_handles();
742        // Ask the process, not just this instance: a spawned thread has its own
743        // WasiModuleInstanceHandles and never sees the main instance's
744        // registration, so `inner.signal_set` alone would apply the default
745        // disposition on sibling threads and terminate a process that does
746        // handle the signal.
747        let handler_registered = inner.signal_set
748            || env
749                .state
750                .signal_handler_registered
751                .load(std::sync::atomic::Ordering::SeqCst);
752        if !handler_registered {
753            let signals = env.thread.pop_signals();
754            if !signals.is_empty() {
755                for sig in signals {
756                    if sig == Signal::Sigint
757                        || sig == Signal::Sigquit
758                        || sig == Signal::Sigkill
759                        || sig == Signal::Sigabrt
760                        || sig == Signal::Sigpipe
761                    {
762                        let exit_code = env.thread.set_or_get_exit_code_for_signal(sig);
763                        return Err(WasiError::Exit(exit_code));
764                    } else {
765                        tracing::trace!(pid=%env.pid(), ?sig, "Signal ignored");
766                    }
767                }
768                return Ok(Ok(true));
769            }
770        }
771
772        // Check for forced exit
773        if let Some(forced_exit) = env.should_exit() {
774            return Err(WasiError::Exit(forced_exit));
775        }
776
777        Self::process_signals(ctx)
778    }
779
780    /// Processes any signals that are batched up
781    pub(crate) fn process_signals(ctx: &mut FunctionEnvMut<'_, Self>) -> WasiResult<bool> {
782        // If a signal handler has never been set then we need to handle signals
783        // differently
784        let env = ctx.data();
785        let env_inner = env
786            .try_inner()
787            .ok_or_else(|| WasiError::Exit(Errno::Fault.into()))?;
788        let inner = env_inner.main_module_instance_handles();
789        if !inner.signal_set {
790            // Process-directed signals are copied into every thread's queue,
791            // but the guest callback belongs only to the instance that
792            // registered it. A sibling instance must discard its copy after
793            // the process-wide registration suppressed the default disposition;
794            // otherwise that copy remains queued forever.
795            let drained = !env.thread.pop_signals().is_empty();
796            return Ok(Ok(drained));
797        }
798
799        // Check for any signals that we need to trigger
800        // (but only if a signal handler is registered)
801        let ret = if inner.signal.as_ref().is_some() {
802            let signals = env.thread.pop_signals();
803            Self::process_signals_internal(ctx, signals)?
804        } else {
805            false
806        };
807
808        Ok(Ok(ret))
809    }
810
811    pub(crate) fn process_signals_internal(
812        ctx: &mut FunctionEnvMut<'_, Self>,
813        mut signals: Vec<Signal>,
814    ) -> Result<bool, WasiError> {
815        let env = ctx.data();
816        let env_inner = env
817            .try_inner()
818            .ok_or_else(|| WasiError::Exit(Errno::Fault.into()))?;
819        let inner = env_inner.main_module_instance_handles();
820        if let Some(handler) = inner.signal.clone() {
821            // We might also have signals that trigger on timers
822            let mut now = 0;
823            {
824                let mut has_signal_interval = false;
825                let mut inner = env.process.inner.0.lock().unwrap();
826                if !inner.signal_intervals.is_empty() {
827                    now = platform_clock_time_get(Snapshot0Clockid::Monotonic, 1_000_000).unwrap()
828                        as u128;
829                    for signal in inner.signal_intervals.values() {
830                        let elapsed = now - signal.last_signal;
831                        if elapsed >= signal.interval.as_nanos() {
832                            has_signal_interval = true;
833                            break;
834                        }
835                    }
836                }
837                if has_signal_interval {
838                    for signal in inner.signal_intervals.values_mut() {
839                        let elapsed = now - signal.last_signal;
840                        if elapsed >= signal.interval.as_nanos() {
841                            signal.last_signal = now;
842                            signals.push(signal.signal);
843                        }
844                    }
845                }
846            }
847
848            for signal in signals {
849                // Skip over Sigwakeup, which is host-side-only
850                if matches!(signal, Signal::Sigwakeup) {
851                    continue;
852                }
853
854                tracing::trace!(
855                    pid=%ctx.data().pid(),
856                    ?signal,
857                    "processing signal via handler",
858                );
859                if let Err(err) = handler.call(ctx, signal as i32) {
860                    match err.downcast::<WasiError>() {
861                        Ok(wasi_err) => {
862                            tracing::warn!(
863                                pid=%ctx.data().pid(),
864                                wasi_err=&wasi_err as &dyn std::error::Error,
865                                "signal handler wasi error",
866                            );
867                            return Err(wasi_err);
868                        }
869                        Err(runtime_err) => {
870                            // anything other than a kill command should report
871                            // the error, killed things may not gracefully close properly
872                            if signal != Signal::Sigkill {
873                                tracing::warn!(
874                                    pid=%ctx.data().pid(),
875                                    runtime_err=&runtime_err as &dyn std::error::Error,
876                                    "signal handler runtime error",
877                                );
878                            }
879                            return Err(WasiError::Exit(Errno::Intr.into()));
880                        }
881                    }
882                }
883                tracing::trace!(
884                    pid=%ctx.data().pid(),
885                    "signal processed",
886                );
887            }
888            Ok(true)
889        } else {
890            tracing::trace!("no signal handler");
891            Ok(false)
892        }
893    }
894
895    /// Returns an exit code if the thread or process has been forced to exit
896    pub fn should_exit(&self) -> Option<ExitCode> {
897        // Check for forced exit
898        if let Some(forced_exit) = self.thread.try_join() {
899            return Some(forced_exit.unwrap_or_else(|err| {
900                tracing::debug!(
901                    error = &*err as &dyn std::error::Error,
902                    "exit runtime error",
903                );
904                Errno::Child.into()
905            }));
906        }
907        if let Some(forced_exit) = self.process.try_join() {
908            return Some(forced_exit.unwrap_or_else(|err| {
909                tracing::debug!(
910                    error = &*err as &dyn std::error::Error,
911                    "exit runtime error",
912                );
913                Errno::Child.into()
914            }));
915        }
916        None
917    }
918
919    /// Accesses the virtual networking implementation
920    pub fn net(&self) -> &DynVirtualNetworking {
921        self.runtime.networking()
922    }
923
924    /// Providers safe access to the initialized part of WasiEnv
925    /// (it must be initialized before it can be used)
926    pub(crate) fn inner(&self) -> WasiInstanceGuard<'_> {
927        self.inner.get().expect(
928            "You must initialize the WasiEnv before using it and can not pass it between threads",
929        )
930    }
931
932    /// Provides safe access to the initialized part of WasiEnv
933    /// (it must be initialized before it can be used)
934    pub(crate) fn inner_mut(&mut self) -> WasiInstanceGuardMut<'_> {
935        self.inner.get_mut().expect(
936            "You must initialize the WasiEnv before using it and can not pass it between threads",
937        )
938    }
939
940    /// Providers safe access to the initialized part of WasiEnv
941    pub(crate) fn try_inner(&self) -> Option<WasiInstanceGuard<'_>> {
942        self.inner.get()
943    }
944
945    /// Providers safe access to the initialized part of WasiEnv
946    /// (it must be initialized before it can be used)
947    #[allow(dead_code)]
948    pub(crate) fn try_inner_mut(&mut self) -> Option<WasiInstanceGuardMut<'_>> {
949        self.inner.get_mut()
950    }
951
952    /// Sets the inner object (this should only be called when
953    /// creating the instance and eventually should be moved out
954    /// of the WasiEnv)
955    #[doc(hidden)]
956    pub(crate) fn set_inner(&mut self, handles: WasiModuleTreeHandles) {
957        self.inner.set(handles);
958        self.refresh_deep_sleep_capability();
959    }
960
961    /// Swaps this inner with the WasiEnvironment of another, this
962    /// is used by the vfork so that the inner handles can be restored
963    /// after the vfork finishes.
964    #[doc(hidden)]
965    pub(crate) fn swap_inner(&mut self, other: &mut Self) {
966        std::mem::swap(&mut self.inner, &mut other.inner);
967    }
968
969    /// Helper function to ensure the module isn't dynamically linked, needed since
970    /// we only support a subset of WASIX functionality for dynamically linked modules.
971    /// Specifically, anything that requires asyncify is not supported right now.
972    pub(crate) fn ensure_static_module(&self) -> Result<(), ()> {
973        self.inner.get().unwrap().ensure_static_module()
974    }
975
976    /// Tries to clone the instance from this environment, but only if it's a static
977    /// module, since dynamically linked modules are made up of multiple instances.
978    pub fn try_clone_instance(&self) -> Option<Instance> {
979        let guard = self.inner.get();
980        match guard {
981            Some(guard) => guard
982                .static_module_instance_handles()
983                .map(|instance| instance.instance.clone()),
984            None => None,
985        }
986    }
987
988    /// Providers safe access to the memory
989    /// (it must be initialized before it can be used)
990    pub fn try_memory(&self) -> Option<WasiInstanceGuardMemory<'_>> {
991        self.try_inner().map(|i| i.memory())
992    }
993
994    /// Providers safe access to the memory
995    /// (it must be initialized before it can be used)
996    ///
997    /// # Safety
998    /// This has been marked as unsafe as it will panic if its executed
999    /// on the wrong thread or before the inner is set
1000    pub unsafe fn memory(&self) -> WasiInstanceGuardMemory<'_> {
1001        self.try_memory().expect(
1002            "You must initialize the WasiEnv before using it and can not pass it between threads",
1003        )
1004    }
1005
1006    /// Providers safe access to the memory
1007    /// (it must be initialized before it can be used)
1008    pub fn try_memory_view<'a>(
1009        &self,
1010        store: &'a (impl AsStoreRef + ?Sized),
1011    ) -> Option<MemoryView<'a>> {
1012        self.try_memory().map(|m| m.view(store))
1013    }
1014
1015    /// Providers safe access to the memory
1016    /// (it must be initialized before it can be used)
1017    ///
1018    /// # Safety
1019    /// This has been marked as unsafe as it will panic if its executed
1020    /// on the wrong thread or before the inner is set
1021    pub unsafe fn memory_view<'a>(&self, store: &'a (impl AsStoreRef + ?Sized)) -> MemoryView<'a> {
1022        self.try_memory_view(store).expect(
1023            "You must initialize the WasiEnv before using it and can not pass it between threads",
1024        )
1025    }
1026
1027    /// Copy the lazy reference so that when it's initialized during the
1028    /// export phase, all the other references get a copy of it
1029    #[allow(dead_code)]
1030    pub(crate) fn try_memory_clone(&self) -> Option<Memory> {
1031        self.try_inner()
1032            .map(|i| i.main_module_instance_handles().memory_clone())
1033    }
1034
1035    /// Get the WASI state
1036    pub(crate) fn state(&self) -> &WasiState {
1037        &self.state
1038    }
1039
1040    /// Get the `VirtualFile` object at stdout
1041    pub fn stdout(&self) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1042        self.state.stdout()
1043    }
1044
1045    /// Get the `VirtualFile` object at stderr
1046    pub fn stderr(&self) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1047        self.state.stderr()
1048    }
1049
1050    /// Get the `VirtualFile` object at stdin
1051    pub fn stdin(&self) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1052        self.state.stdin()
1053    }
1054
1055    /// Returns true if the process should perform snapshots or not
1056    pub fn should_journal(&self) -> bool {
1057        self.enable_journal && !self.replaying_journal
1058    }
1059
1060    /// Returns true if the environment has an active journal
1061    #[cfg(feature = "journal")]
1062    pub fn has_active_journal(&self) -> bool {
1063        self.runtime().active_journal().is_some()
1064    }
1065
1066    /// Returns the active journal or fails with an error
1067    #[cfg(feature = "journal")]
1068    pub fn active_journal(&self) -> Result<&DynJournal, Errno> {
1069        self.runtime().active_journal().ok_or_else(|| {
1070            tracing::debug!("failed to save thread exit as there is not active journal");
1071            Errno::Fault
1072        })
1073    }
1074
1075    /// Returns true if a particular snapshot trigger is enabled
1076    #[cfg(feature = "journal")]
1077    pub fn has_snapshot_trigger(&self, trigger: SnapshotTrigger) -> bool {
1078        let guard = self.process.inner.0.lock().unwrap();
1079        guard.snapshot_on.contains(&trigger)
1080    }
1081
1082    /// Returns true if a particular snapshot trigger is enabled
1083    #[cfg(feature = "journal")]
1084    pub fn pop_snapshot_trigger(&mut self, trigger: SnapshotTrigger) -> bool {
1085        let mut guard = self.process.inner.0.lock().unwrap();
1086        if trigger.only_once() {
1087            guard.snapshot_on.remove(&trigger)
1088        } else {
1089            guard.snapshot_on.contains(&trigger)
1090        }
1091    }
1092
1093    /// Internal helper function to get a standard device handle.
1094    /// Expects one of `__WASI_STDIN_FILENO`, `__WASI_STDOUT_FILENO`, `__WASI_STDERR_FILENO`.
1095    pub fn std_dev_get(
1096        &self,
1097        fd: crate::syscalls::WasiFd,
1098    ) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1099        self.state.std_dev_get(fd)
1100    }
1101
1102    /// Unsafe:
1103    ///
1104    /// This will access the memory of the WASM process and create a view into it which is
1105    /// inherently unsafe as it could corrupt the memory. Also accessing the memory is not
1106    /// thread safe.
1107    pub(crate) unsafe fn get_memory_and_wasi_state<'a>(
1108        &'a self,
1109        store: &'a impl AsStoreRef,
1110        _mem_index: u32,
1111    ) -> (MemoryView<'a>, &'a WasiState) {
1112        let memory = unsafe { self.memory_view(store) };
1113        let state = self.state.deref();
1114        (memory, state)
1115    }
1116
1117    /// Unsafe:
1118    ///
1119    /// This will access the memory of the WASM process and create a view into it which is
1120    /// inherently unsafe as it could corrupt the memory. Also accessing the memory is not
1121    /// thread safe.
1122    pub(crate) unsafe fn get_memory_and_wasi_state_and_inodes<'a>(
1123        &'a self,
1124        store: &'a impl AsStoreRef,
1125        _mem_index: u32,
1126    ) -> (MemoryView<'a>, &'a WasiState, &'a WasiInodes) {
1127        let memory = unsafe { self.memory_view(store) };
1128        let state = self.state.deref();
1129        let inodes = &state.inodes;
1130        (memory, state, inodes)
1131    }
1132
1133    pub(crate) fn get_wasi_state_and_inodes(&self) -> (&WasiState, &WasiInodes) {
1134        let state = self.state.deref();
1135        let inodes = &state.inodes;
1136        (state, inodes)
1137    }
1138
1139    pub(crate) fn get_wasi_state(&self) -> &WasiState {
1140        self.state.deref()
1141    }
1142
1143    pub fn use_package(&self, pkg: &BinaryPackage) -> Result<(), WasiStateCreationError> {
1144        block_on(self.use_package_async(pkg))
1145    }
1146
1147    /// Make all the commands in a [`BinaryPackage`] available to the WASI
1148    /// instance.
1149    ///
1150    /// The [`BinaryPackageCommand::atom()`][cmd-atom] will be saved to
1151    /// `/bin/<command>` and `/usr/bin/<command>`. Each path is registered with
1152    /// the package independently, so a failure to write one does not affect
1153    /// the other.
1154    ///
1155    /// This will also merge the package's mount manifest
1156    /// ([`BinaryPackage::package_mounts`][pkg-fs]) into the current filesystem.
1157    ///
1158    /// [cmd-atom]: crate::bin_factory::BinaryPackageCommand::atom()
1159    /// [pkg-fs]: crate::bin_factory::BinaryPackage::package_mounts
1160    pub async fn use_package_async(
1161        &self,
1162        pkg: &BinaryPackage,
1163    ) -> Result<(), WasiStateCreationError> {
1164        tracing::trace!(package=%pkg.id, "merging package dependency into wasi environment");
1165        let root_fs = &self.state.fs.root_fs;
1166
1167        // We first need to merge the package mounts into the main
1168        // filesystem, if they have not been merged already.
1169        if let Err(e) = self.state.fs.conditional_union(pkg).await {
1170            tracing::warn!(
1171                error = &e as &dyn std::error::Error,
1172                "Unable to merge the package mounts into the main filesystem",
1173            );
1174        }
1175
1176        // Next, make sure all commands will be available
1177
1178        if !pkg.commands.is_empty() {
1179            let _ = root_fs.create_dir(Path::new("/bin"));
1180            let _ = root_fs.create_dir(Path::new("/usr"));
1181            let _ = root_fs.create_dir(Path::new("/usr/bin"));
1182
1183            for command in &pkg.commands {
1184                let atom = command.atom();
1185                let mut package = pkg.clone();
1186                package.entrypoint_cmd = Some(command.name().to_string());
1187                let package_arc = Arc::new(package);
1188
1189                for directory in ["/bin", "/usr/bin"] {
1190                    let path = format!("{directory}/{}", command.name());
1191                    if let Err(err) =
1192                        write_readonly_buffer_to_fs(root_fs, Path::new(&path), &atom).await
1193                    {
1194                        tracing::debug!(
1195                            package=%pkg.id,
1196                            command_name=command.name(),
1197                            path,
1198                            error=%err,
1199                            "Failed to inject a command into the filesystem",
1200                        );
1201                        continue;
1202                    }
1203
1204                    // Keep metadata for each installed path even if another
1205                    // alias cannot be written (for example, below a mount).
1206                    self.bin_factory.set_binary(&path, &package_arc);
1207                    tracing::debug!(
1208                        package=%pkg.id,
1209                        command_name=command.name(),
1210                        path,
1211                        "Injected a command into the filesystem",
1212                    );
1213                }
1214            }
1215        }
1216
1217        Ok(())
1218    }
1219
1220    /// Given a list of packages, load them from the registry and make them
1221    /// available.
1222    pub fn uses<I>(&self, uses: I) -> Result<(), WasiStateCreationError>
1223    where
1224        I: IntoIterator<Item = String>,
1225    {
1226        let rt = self.runtime();
1227
1228        for package_name in uses {
1229            let specifier = package_name.parse::<PackageSource>().map_err(|e| {
1230                WasiStateCreationError::WasiIncludePackageError(format!(
1231                    "package_name={package_name}, {e}",
1232                ))
1233            })?;
1234            let pkg = block_on(BinaryPackage::from_registry(&specifier, rt)).map_err(|e| {
1235                WasiStateCreationError::WasiIncludePackageError(format!(
1236                    "package_name={package_name}, {e}",
1237                ))
1238            })?;
1239            self.use_package(&pkg)?;
1240        }
1241
1242        Ok(())
1243    }
1244
1245    #[cfg(feature = "sys")]
1246    pub fn map_commands(
1247        &self,
1248        map_commands: std::collections::HashMap<String, std::path::PathBuf>,
1249    ) -> Result<(), WasiStateCreationError> {
1250        // Load all the mapped atoms
1251        #[allow(unused_imports)]
1252        use std::path::Path;
1253
1254        use shared_buffer::OwnedBuffer;
1255        #[allow(unused_imports)]
1256        use virtual_fs::FileSystem;
1257
1258        #[cfg(feature = "sys")]
1259        for (command, target) in map_commands.iter() {
1260            // Read the file
1261            let file = std::fs::read(target).map_err(|err| {
1262                WasiStateCreationError::WasiInheritError(format!(
1263                    "failed to read local binary [{}] - {}",
1264                    target.as_os_str().to_string_lossy(),
1265                    err
1266                ))
1267            })?;
1268            let file = OwnedBuffer::from(file);
1269
1270            let path = format!("/bin/{command}");
1271            let path = Path::new(path.as_str());
1272            if let Err(err) = block_on(write_readonly_buffer_to_fs(
1273                &self.state.fs.root_fs,
1274                path,
1275                &file,
1276            )) {
1277                tracing::debug!("failed to add atom command [{}] - {}", command, err);
1278                continue;
1279            }
1280
1281            let path = format!("/usr/bin/{command}");
1282            let path = Path::new(path.as_str());
1283            if let Err(err) = block_on(write_readonly_buffer_to_fs(
1284                &self.state.fs.root_fs,
1285                path,
1286                &file,
1287            )) {
1288                tracing::debug!("failed to add atom command [{}] - {}", command, err);
1289                continue;
1290            }
1291        }
1292        Ok(())
1293    }
1294
1295    /// Cleans up all the open files (if this is the main thread)
1296    #[allow(clippy::await_holding_lock)]
1297    pub fn blocking_on_exit(&self, process_exit_code: Option<ExitCode>) {
1298        let cleanup = self.on_exit(process_exit_code);
1299        block_on(cleanup);
1300    }
1301
1302    /// Cleans up all the open files (if this is the main thread)
1303    #[allow(clippy::await_holding_lock)]
1304    pub fn on_exit(&self, process_exit_code: Option<ExitCode>) -> BoxFuture<'static, ()> {
1305        const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
1306
1307        // If snap-shooting is enabled then we should record an event that the thread has exited.
1308        #[cfg(feature = "journal")]
1309        if self.should_journal() && self.has_active_journal() {
1310            if let Err(err) = JournalEffector::save_thread_exit(self, self.tid(), process_exit_code)
1311            {
1312                tracing::warn!("failed to save snapshot event for thread exit - {}", err);
1313            }
1314
1315            if self.thread.is_main()
1316                && let Err(err) = JournalEffector::save_process_exit(self, process_exit_code)
1317            {
1318                tracing::warn!("failed to save snapshot event for process exit - {}", err);
1319            }
1320        }
1321
1322        // If the process wants to exit, also close all files and terminate it
1323        if let Some(process_exit_code) = process_exit_code {
1324            let process = self.process.clone();
1325            let disable_fs_cleanup = self.disable_fs_cleanup;
1326            let pid = self.pid();
1327
1328            let timeout = self.tasks().sleep_now(CLEANUP_TIMEOUT);
1329            let state = self.state.clone();
1330            Box::pin(async move {
1331                if process.try_start_cleanup() {
1332                    if !disable_fs_cleanup {
1333                        tracing::trace!(pid = %pid, "cleaning up open file handles");
1334
1335                        // Perform the clean operation using the asynchronous runtime
1336                        tokio::select! {
1337                            _ = timeout => {
1338                                tracing::debug!(
1339                                    "WasiEnv::cleanup has timed out after {CLEANUP_TIMEOUT:?}"
1340                                );
1341                            },
1342                            _ = state.fs.close_all() => { }
1343                        }
1344                    }
1345
1346                    // Record the real exit code before broadcasting Sigquit.
1347                    // Otherwise a pending Sigquit can win the status race and
1348                    // make waiters observe a successful exit.
1349                    process.terminate(process_exit_code);
1350                    process.signal_process(Signal::Sigquit);
1351                }
1352            })
1353        } else {
1354            Box::pin(async {})
1355        }
1356    }
1357
1358    pub fn prepare_spawn(&self, cmd: &BinaryPackageCommand) {
1359        if let Ok(Some(Wasi {
1360            main_args,
1361            env: env_vars,
1362            exec_name,
1363            ..
1364        })) = cmd.metadata().wasi()
1365        {
1366            if let Some(env_vars) = env_vars {
1367                add_command_env_defaults(&mut self.state.envs.lock().unwrap(), env_vars);
1368            }
1369
1370            if let Some(main_args) = main_args {
1371                let mut args: std::sync::MutexGuard<'_, Vec<String>> =
1372                    self.state.args.lock().unwrap();
1373                // Insert main-args before user args
1374                args.splice(1..1, main_args);
1375            }
1376
1377            if let Some(exec_name) = exec_name {
1378                self.state.args.lock().unwrap()[0] = exec_name;
1379            }
1380        }
1381    }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386    use super::*;
1387    use once_cell::sync::OnceCell;
1388    use shared_buffer::OwnedBuffer;
1389    use virtual_fs::{RootFileSystemBuilder, TmpFileSystem};
1390    use wasmer::Engine;
1391    use wasmer_config::package::PackageId;
1392
1393    #[tokio::test]
1394    async fn command_alias_registration_is_independent() {
1395        let id = PackageId::new_named("test/command-args", "1.0.0".parse().unwrap());
1396        let command = BinaryPackageCommand::new(
1397            "pi".to_string(),
1398            serde_json::from_value(serde_json::json!({
1399                "runner": "wasi",
1400                "annotations": { "wasi": {
1401                    "atom": "pi", "main-args": ["/opt/pi/cli.js"]
1402                } }
1403            }))
1404            .unwrap(),
1405            OwnedBuffer::from(b"\0asm\x01\0\0\0".to_vec()),
1406            ModuleHash::random(),
1407            None,
1408            id.clone(),
1409            id.clone(),
1410        );
1411        let package = BinaryPackage {
1412            id,
1413            package_ids: vec![],
1414            webc_version: webc::Version::V3,
1415            when_cached: None,
1416            entrypoint_cmd: Some("pi".to_string()),
1417            hash: OnceCell::new(),
1418            package_mounts: None,
1419            commands: vec![command],
1420            uses: vec![],
1421            additional_host_mapped_directories: vec![],
1422        };
1423
1424        // A mount below a command directory (`/usr/local/ssl`) must not stop
1425        // installation; a mount *at* one hides that alias, which must then
1426        // stay unregistered without affecting the other alias.
1427        for (mount, available, unavailable) in [
1428            ("/usr/local/ssl", &["/bin/pi", "/usr/bin/pi"][..], &[][..]),
1429            ("/usr/bin", &["/bin/pi"][..], &["/usr/bin/pi"][..]),
1430            ("/bin", &["/usr/bin/pi"][..], &["/bin/pi"][..]),
1431        ] {
1432            let env = WasiEnvBuilder::new("pi")
1433                .arg("--provider")
1434                .arg("openai")
1435                .engine(Engine::default())
1436                .fs(
1437                    Arc::new(RootFileSystemBuilder::default().build_tmp_ext(&["/bin"]))
1438                        as Arc<dyn FileSystem + Send + Sync>,
1439                )
1440                .build()
1441                .unwrap();
1442            let fs = &env.state.fs.root_fs;
1443            fs.root()
1444                .mount(Path::new(mount), Arc::new(TmpFileSystem::new()))
1445                .unwrap();
1446
1447            env.use_package_async(&package).await.unwrap();
1448            for path in unavailable {
1449                assert!(fs.metadata(Path::new(path)).is_err(), "{path}");
1450                assert!(
1451                    env.bin_factory.get_binary(path, None).await.is_none(),
1452                    "{path}"
1453                );
1454            }
1455            let mut installed = Vec::new();
1456            for path in available {
1457                assert!(fs.metadata(Path::new(path)).unwrap().is_file(), "{path}");
1458                installed.push(
1459                    env.bin_factory
1460                        .get_binary(path, None)
1461                        .await
1462                        .expect("a successfully installed alias must retain its package metadata"),
1463                );
1464            }
1465            // `prepare_spawn` mutates the env's args, so apply it only once.
1466            env.prepare_spawn(installed[0].get_command("pi").unwrap());
1467            assert_eq!(
1468                *env.state.args.lock().unwrap(),
1469                ["pi", "/opt/pi/cli.js", "--provider", "openai"]
1470            );
1471        }
1472    }
1473
1474    #[test]
1475    fn spawned_command_environment_is_default_only() {
1476        let mut environment = vec![b"HOME=/workspace".to_vec(), b"PATH=/bin".to_vec()];
1477
1478        add_command_env_defaults(
1479            &mut environment,
1480            vec![
1481                "HOME=/package".to_owned(),
1482                "PREFIX=/".to_owned(),
1483                "PREFIX=/duplicate".to_owned(),
1484            ],
1485        );
1486
1487        assert_eq!(
1488            environment,
1489            vec![
1490                b"HOME=/workspace".to_vec(),
1491                b"PATH=/bin".to_vec(),
1492                b"PREFIX=/".to_vec(),
1493            ]
1494        );
1495    }
1496}