1use std::{path::PathBuf, sync::Arc};
4
5use anyhow::{Context, Error};
6use tracing::Instrument;
7use virtual_fs::{ArcBoxFile, FileSystem, VirtualFile};
8use wasmer::{Engine, Module};
9use wasmer_types::ModuleHash;
10use webc::metadata::{Command, annotations::Wasi};
11
12use crate::{
13 Runtime, WasiEnvBuilder, WasiError, WasiRuntimeError,
14 bin_factory::BinaryPackage,
15 capabilities::Capabilities,
16 journal::{DynJournal, DynReadableJournal, SnapshotTrigger},
17 runners::{MappedDirectory, MountedDirectory, wasi_common::CommonWasiOptions},
18 runtime::task_manager::VirtualTaskManagerExt,
19};
20
21use super::wasi_common::{
22 ExistingMountConflictBehavior, MAPPED_CURRENT_DIR_DEFAULT_PATH, MappedCommand,
23};
24
25#[derive(Debug, Default, Clone)]
26pub struct WasiRunner {
27 wasi: CommonWasiOptions,
28 stdin: Option<ArcBoxFile>,
29 stdout: Option<ArcBoxFile>,
30 stderr: Option<ArcBoxFile>,
31}
32
33pub enum PackageOrHash<'a> {
34 Package(&'a BinaryPackage),
35 Hash(ModuleHash),
36}
37
38pub enum RuntimeOrEngine {
39 Runtime(Arc<dyn Runtime + Send + Sync>),
40 Engine(Engine),
41}
42
43impl WasiRunner {
44 pub fn new() -> Self {
46 WasiRunner::default()
47 }
48
49 pub fn entry_function(&self) -> Option<String> {
51 self.wasi.entry_function.clone()
52 }
53
54 pub fn with_entry_function<S>(&mut self, entry_function: S) -> &mut Self
56 where
57 S: Into<String>,
58 {
59 self.wasi.entry_function = Some(entry_function.into());
60 self
61 }
62
63 pub fn get_args(&self) -> Vec<String> {
65 self.wasi.args.clone()
66 }
67
68 pub fn with_args<A, S>(&mut self, args: A) -> &mut Self
70 where
71 A: IntoIterator<Item = S>,
72 S: Into<String>,
73 {
74 self.wasi.args = args.into_iter().map(|s| s.into()).collect();
75 self
76 }
77
78 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80 self.wasi.env.insert(key.into(), value.into());
81 self
82 }
83
84 pub fn with_envs<I, K, V>(&mut self, envs: I) -> &mut Self
85 where
86 I: IntoIterator<Item = (K, V)>,
87 K: Into<String>,
88 V: Into<String>,
89 {
90 for (key, value) in envs {
91 self.wasi.env.insert(key.into(), value.into());
92 }
93 self
94 }
95
96 pub fn with_forward_host_env(&mut self, forward: bool) -> &mut Self {
97 self.wasi.forward_host_env = forward;
98 self
99 }
100
101 pub fn with_mapped_directories<I, D>(&mut self, dirs: I) -> &mut Self
102 where
103 I: IntoIterator<Item = D>,
104 D: Into<MappedDirectory>,
105 {
106 self.with_mounted_directories(dirs.into_iter().map(Into::into).map(MountedDirectory::from))
107 }
108
109 pub fn with_home_mapped(&mut self, is_home_mapped: bool) -> &mut Self {
110 self.wasi.is_home_mapped = is_home_mapped;
111 self
112 }
113
114 pub fn with_mounted_directories<I, D>(&mut self, dirs: I) -> &mut Self
115 where
116 I: IntoIterator<Item = D>,
117 D: Into<MountedDirectory>,
118 {
119 self.wasi.mounts.extend(dirs.into_iter().map(Into::into));
120 self
121 }
122
123 pub fn with_mount(&mut self, dest: String, fs: Arc<dyn FileSystem + Send + Sync>) -> &mut Self {
125 self.wasi.mounts.push(MountedDirectory { guest: dest, fs });
126 self
127 }
128
129 pub fn with_existing_mount_conflict_behavior(
130 &mut self,
131 behavior: ExistingMountConflictBehavior,
132 ) -> &mut Self {
133 self.wasi.existing_mount_conflict_behavior = behavior;
134 self
135 }
136
137 pub fn with_current_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
139 self.wasi.current_dir = Some(dir.into());
140 self
141 }
142
143 pub fn with_injected_package(&mut self, pkg: BinaryPackage) -> &mut Self {
145 self.wasi.injected_packages.push(pkg);
146 self
147 }
148
149 pub fn with_injected_packages(
151 &mut self,
152 packages: impl IntoIterator<Item = BinaryPackage>,
153 ) -> &mut Self {
154 self.wasi.injected_packages.extend(packages);
155 self
156 }
157
158 pub fn with_mapped_host_command(
159 &mut self,
160 alias: impl Into<String>,
161 target: impl Into<String>,
162 ) -> &mut Self {
163 self.wasi.mapped_host_commands.push(MappedCommand {
164 alias: alias.into(),
165 target: target.into(),
166 });
167 self
168 }
169
170 pub fn with_mapped_host_commands(
171 &mut self,
172 commands: impl IntoIterator<Item = MappedCommand>,
173 ) -> &mut Self {
174 self.wasi.mapped_host_commands.extend(commands);
175 self
176 }
177
178 pub fn capabilities_mut(&mut self) -> &mut Capabilities {
179 &mut self.wasi.capabilities
180 }
181
182 pub fn with_capabilities(&mut self, capabilities: Capabilities) -> &mut Self {
183 self.wasi.capabilities = capabilities;
184 self
185 }
186
187 #[cfg(feature = "journal")]
188 pub fn with_snapshot_trigger(&mut self, on: SnapshotTrigger) -> &mut Self {
189 self.wasi.snapshot_on.push(on);
190 self
191 }
192
193 #[cfg(feature = "journal")]
194 pub fn with_default_snapshot_triggers(&mut self) -> &mut Self {
195 for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
196 if !self.has_snapshot_trigger(on) {
197 self.with_snapshot_trigger(on);
198 }
199 }
200 self
201 }
202
203 #[cfg(feature = "journal")]
204 pub fn has_snapshot_trigger(&self, on: SnapshotTrigger) -> bool {
205 self.wasi.snapshot_on.contains(&on)
206 }
207
208 #[cfg(feature = "journal")]
209 pub fn with_snapshot_interval(&mut self, period: std::time::Duration) -> &mut Self {
210 if !self.has_snapshot_trigger(SnapshotTrigger::PeriodicInterval) {
211 self.with_snapshot_trigger(SnapshotTrigger::PeriodicInterval);
212 }
213 self.wasi.snapshot_interval.replace(period);
214 self
215 }
216
217 #[cfg(feature = "journal")]
218 pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) -> &mut Self {
219 self.wasi.stop_running_after_snapshot = stop_running;
220 self
221 }
222
223 #[cfg(feature = "journal")]
224 pub fn with_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) -> &mut Self {
225 self.wasi.read_only_journals.push(journal);
226 self
227 }
228
229 #[cfg(feature = "journal")]
230 pub fn with_writable_journal(&mut self, journal: Arc<DynJournal>) -> &mut Self {
231 self.wasi.writable_journals.push(journal);
232 self
233 }
234
235 pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) -> &mut Self {
236 self.wasi.skip_stdio_during_bootstrap = skip;
237 self
238 }
239
240 pub fn with_stdin(&mut self, stdin: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
241 self.stdin = Some(ArcBoxFile::new(stdin));
242 self
243 }
244
245 pub fn with_stdout(&mut self, stdout: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
246 self.stdout = Some(ArcBoxFile::new(stdout));
247 self
248 }
249
250 pub fn with_stderr(&mut self, stderr: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
251 self.stderr = Some(ArcBoxFile::new(stderr));
252 self
253 }
254
255 fn ensure_tokio_runtime() -> Option<tokio::runtime::Runtime> {
256 #[cfg(feature = "sys-thread")]
257 {
258 if tokio::runtime::Handle::try_current().is_ok() {
259 return None;
260 }
261
262 let rt = tokio::runtime::Builder::new_multi_thread()
263 .enable_all()
264 .build()
265 .expect(
266 "Failed to build a multi-threaded tokio runtime. This is necessary \
267 for WASIX to work. You can provide a tokio runtime by building one \
268 yourself and entering it before using WasiRunner.",
269 );
270 Some(rt)
271 }
272
273 #[cfg(not(feature = "sys-thread"))]
274 {
275 None
276 }
277 }
278
279 pub fn with_asynchronous_threading(
280 &mut self,
281 enable_asynchronous_threading: bool,
282 ) -> &mut Self {
283 self.wasi
284 .capabilities
285 .threading
286 .enable_asynchronous_threading = enable_asynchronous_threading;
287 self
288 }
289
290 #[tracing::instrument(level = "debug", skip_all)]
291 pub fn prepare_webc_env(
292 &self,
293 program_name: &str,
294 wasi: &Wasi,
295 pkg_or_hash: PackageOrHash,
296 runtime_or_engine: RuntimeOrEngine,
297 root_fs: Option<crate::fs::WasiFsRoot>,
298 ) -> Result<WasiEnvBuilder, anyhow::Error> {
299 let mut builder = WasiEnvBuilder::new(program_name);
300
301 match runtime_or_engine {
302 RuntimeOrEngine::Runtime(runtime) => {
303 builder.set_runtime(runtime);
304 }
305 RuntimeOrEngine::Engine(engine) => {
306 builder.set_engine(engine);
307 }
308 }
309
310 let container_mounts = match pkg_or_hash {
311 PackageOrHash::Package(pkg) => {
312 builder.add_webc(pkg.clone());
313 builder.set_module_hash(pkg.hash());
314 builder.include_packages(pkg.package_ids.clone());
315
316 pkg.package_mounts.as_deref()
317 }
318 PackageOrHash::Hash(hash) => {
319 builder.set_module_hash(hash);
320 None
321 }
322 };
323
324 if self.wasi.is_home_mapped {
325 builder.set_current_dir(MAPPED_CURRENT_DIR_DEFAULT_PATH);
326 }
327
328 if let Some(current_dir) = &self.wasi.current_dir {
329 builder.set_current_dir(current_dir.clone());
330 }
331
332 if let Some(cwd) = &wasi.cwd {
333 builder.set_current_dir(cwd);
334 }
335
336 self.wasi
337 .prepare_webc_env(&mut builder, container_mounts, wasi, root_fs)?;
338
339 if let Some(stdin) = &self.stdin {
340 builder.set_stdin(Box::new(stdin.clone()));
341 }
342 if let Some(stdout) = &self.stdout {
343 builder.set_stdout(Box::new(stdout.clone()));
344 }
345 if let Some(stderr) = &self.stderr {
346 builder.set_stderr(Box::new(stderr.clone()));
347 }
348
349 Ok(builder)
350 }
351
352 pub fn run_wasm(
353 &self,
354 runtime_or_engine: RuntimeOrEngine,
355 program_name: &str,
356 module: Module,
357 module_hash: ModuleHash,
358 ) -> Result<(), Error> {
359 let tokio_runtime = Self::ensure_tokio_runtime();
361 let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
362
363 let runner = self.clone();
364
365 let wasi = webc::metadata::annotations::Wasi::new(program_name);
366
367 let mut builder = runner.prepare_webc_env(
368 program_name,
369 &wasi,
370 PackageOrHash::Hash(module_hash),
371 runtime_or_engine,
372 None,
373 )?;
374
375 #[cfg(feature = "ctrlc")]
376 {
377 builder = builder.attach_ctrl_c();
378 }
379
380 #[cfg(feature = "journal")]
381 {
382 for journal in runner.wasi.read_only_journals.iter().cloned() {
383 builder.add_read_only_journal(journal);
384 }
385 for journal in runner.wasi.writable_journals.iter().cloned() {
386 builder.add_writable_journal(journal);
387 }
388
389 if !runner.wasi.snapshot_on.is_empty() {
390 for trigger in runner.wasi.snapshot_on.iter().cloned() {
391 builder.add_snapshot_trigger(trigger);
392 }
393 } else if !runner.wasi.writable_journals.is_empty() {
394 for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
395 builder.add_snapshot_trigger(on);
396 }
397 }
398
399 if let Some(period) = runner.wasi.snapshot_interval {
400 if runner.wasi.writable_journals.is_empty() {
401 return Err(anyhow::format_err!(
402 "If you specify a snapshot interval then you must also specify a writable journal file"
403 ));
404 }
405 builder.with_snapshot_interval(period);
406 }
407
408 builder.with_stop_running_after_snapshot(runner.wasi.stop_running_after_snapshot);
409 builder.with_skip_stdio_during_bootstrap(runner.wasi.skip_stdio_during_bootstrap);
410 }
411
412 let env = builder.build()?;
413 let runtime = env.runtime.clone();
414 let tasks = runtime.task_manager().clone();
415
416 let mut task_handle =
417 crate::bin_factory::spawn_exec_module(module, env, &runtime).context("Spawn failed")?;
418
419 #[cfg(feature = "ctrlc")]
420 task_handle.install_ctrlc_handler();
421 let task_handle = async move { task_handle.wait_finished().await }.in_current_span();
422
423 let result = tasks.spawn_and_block_on(task_handle)?;
424 let exit_code = result
425 .map_err(|err| {
426 let msg = err.to_string();
428 let weak = Arc::downgrade(&err);
429 Arc::into_inner(err).unwrap_or_else(|| {
430 weak.upgrade()
431 .map(|err| wasi_runtime_error_to_owned(&err))
432 .unwrap_or_else(|| {
433 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
434 })
435 })
436 })
437 .context("Unable to wait for the process to exit")?;
438
439 if exit_code.raw() == 0 {
440 Ok(())
441 } else {
442 Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
443 }
444 }
445
446 pub fn run_command(
447 &mut self,
448 command_name: &str,
449 pkg: &BinaryPackage,
450 runtime_or_engine: RuntimeOrEngine,
451 ) -> Result<(), Error> {
452 let tokio_runtime = Self::ensure_tokio_runtime();
454 let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
455
456 let cmd = pkg
457 .get_command(command_name)
458 .with_context(|| format!("The package doesn't contain a \"{command_name}\" command"))?;
459
460 let wasi = cmd
461 .metadata()
462 .annotation("wasi")?
463 .unwrap_or_else(|| Wasi::new(command_name));
464
465 let exec_name = if let Some(exec_name) = wasi.exec_name.as_ref() {
466 exec_name
467 } else {
468 command_name
469 };
470
471 #[allow(unused_mut)]
472 let mut builder = self
473 .prepare_webc_env(
474 exec_name,
475 &wasi,
476 PackageOrHash::Package(pkg),
477 runtime_or_engine,
478 None,
479 )
480 .context("Unable to prepare the WASI environment")?;
481
482 #[cfg(feature = "journal")]
483 {
484 for journal in self.wasi.read_only_journals.iter().cloned() {
485 builder.add_read_only_journal(journal);
486 }
487 for journal in self.wasi.writable_journals.iter().cloned() {
488 builder.add_writable_journal(journal);
489 }
490
491 if !self.wasi.snapshot_on.is_empty() {
492 for trigger in self.wasi.snapshot_on.iter().cloned() {
493 builder.add_snapshot_trigger(trigger);
494 }
495 } else if !self.wasi.writable_journals.is_empty() {
496 for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
497 builder.add_snapshot_trigger(on);
498 }
499 }
500
501 if let Some(period) = self.wasi.snapshot_interval {
502 if self.wasi.writable_journals.is_empty() {
503 return Err(anyhow::format_err!(
504 "If you specify a snapshot interval then you must also specify a journal file"
505 ));
506 }
507 builder.with_snapshot_interval(period);
508 }
509
510 builder.with_stop_running_after_snapshot(self.wasi.stop_running_after_snapshot);
511 }
512
513 let env = builder.build()?;
514 let runtime = env.runtime.clone();
515 let command_name = command_name.to_string();
516 let tasks = runtime.task_manager().clone();
517 let pkg = pkg.clone();
518
519 let exit_code = tasks.spawn_and_block_on(
524 async move {
525 let mut task_handle =
526 crate::bin_factory::spawn_exec(pkg, &command_name, env, &runtime)
527 .await
528 .context("Spawn failed")?;
529
530 #[cfg(feature = "ctrlc")]
531 task_handle.install_ctrlc_handler();
532
533 task_handle
534 .wait_finished()
535 .await
536 .map_err(|err| {
537 let msg = err.to_string();
539 let weak = Arc::downgrade(&err);
540 Arc::into_inner(err).unwrap_or_else(|| {
541 weak.upgrade()
542 .map(|err| wasi_runtime_error_to_owned(&err))
543 .unwrap_or_else(|| {
544 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
545 })
546 })
547 })
548 .context("Unable to wait for the process to exit")
549 }
550 .in_current_span(),
551 )??;
552
553 if exit_code.raw() == 0 {
554 Ok(())
555 } else {
556 Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
557 }
558 }
559}
560
561impl crate::runners::Runner for WasiRunner {
562 fn can_run_command(command: &Command) -> Result<bool, Error> {
563 Ok(command
564 .runner
565 .starts_with(webc::metadata::annotations::WASI_RUNNER_URI))
566 }
567
568 #[tracing::instrument(skip_all)]
569 fn run_command(
570 &mut self,
571 command_name: &str,
572 pkg: &BinaryPackage,
573 runtime: Arc<dyn Runtime + Send + Sync>,
574 ) -> Result<(), Error> {
575 self.run_command(command_name, pkg, RuntimeOrEngine::Runtime(runtime))
576 }
577}
578
579fn wasi_runtime_error_to_owned(err: &WasiRuntimeError) -> WasiRuntimeError {
580 match err {
581 WasiRuntimeError::Init(a) => WasiRuntimeError::Init(a.clone()),
582 WasiRuntimeError::Export(a) => WasiRuntimeError::Export(a.clone()),
583 WasiRuntimeError::Instantiation(a) => WasiRuntimeError::Instantiation(a.clone()),
584 WasiRuntimeError::Wasi(WasiError::Exit(a)) => WasiRuntimeError::Wasi(WasiError::Exit(*a)),
585 WasiRuntimeError::Wasi(WasiError::ThreadExit) => {
586 WasiRuntimeError::Wasi(WasiError::ThreadExit)
587 }
588 WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion) => {
589 WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion)
590 }
591 WasiRuntimeError::Wasi(WasiError::DeepSleep(_)) => {
592 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("deep-sleep")))
593 }
594 WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol)) => {
595 WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol.clone()))
596 }
597 WasiRuntimeError::ControlPlane(a) => WasiRuntimeError::ControlPlane(a.clone()),
598 WasiRuntimeError::Runtime(a) => WasiRuntimeError::Runtime(a.clone()),
599 WasiRuntimeError::Thread(a) => WasiRuntimeError::Thread(a.clone()),
600 WasiRuntimeError::Anyhow(a) => WasiRuntimeError::Anyhow(a.clone()),
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn send_and_sync() {
610 fn assert_send<T: Send>() {}
611 fn assert_sync<T: Sync>() {}
612
613 assert_send::<WasiRunner>();
614 assert_sync::<WasiRunner>();
615 }
616
617 #[cfg(all(feature = "host-fs", feature = "sys"))]
618 #[tokio::test]
619 async fn test_volume_mount_without_webcs() {
620 use std::sync::Arc;
621
622 let tokrt = tokio::runtime::Handle::current();
623
624 let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
625 let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
626
627 let mut envb = crate::runners::wasi::WasiRunner::new();
628 envb.with_mount("/host".to_string(), hostdir_dyn);
629
630 let annotations = webc::metadata::annotations::Wasi::new("test");
631
632 let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
633 tokrt.clone(),
634 ));
635 let rt = crate::PluggableRuntime::new(tm);
636
637 let envb = envb
638 .prepare_webc_env(
639 "test",
640 &annotations,
641 PackageOrHash::Hash(ModuleHash::random()),
642 RuntimeOrEngine::Runtime(Arc::new(rt)),
643 None,
644 )
645 .unwrap();
646
647 let init = envb.build_init().unwrap();
648
649 let fs = &init.state.fs.root_fs;
650
651 fs.read_dir(std::path::Path::new("/host")).unwrap();
652 }
653
654 #[cfg(all(feature = "host-fs", feature = "sys"))]
655 #[tokio::test]
656 async fn test_volume_mount_with_webcs() {
657 use std::sync::Arc;
658
659 use wasmer_package::utils::from_bytes;
660
661 let tokrt = tokio::runtime::Handle::current();
662
663 let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
664 let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
665
666 let mut envb = crate::runners::wasi::WasiRunner::new();
667 envb.with_mount("/host".to_string(), hostdir_dyn);
668
669 let annotations = webc::metadata::annotations::Wasi::new("test");
670
671 let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
672 tokrt.clone(),
673 ));
674 let mut rt = crate::PluggableRuntime::new(tm);
675 rt.set_package_loader(crate::runtime::package_loader::BuiltinPackageLoader::new());
676
677 let webc_path = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
678 .join("../../wasmer-test-files/integration/webc/wasmer-tests--volume-static-webserver@0.1.0.webc");
679 let webc_data = std::fs::read(webc_path).unwrap();
680 let container = from_bytes(webc_data).unwrap();
681
682 let binpkg = crate::bin_factory::BinaryPackage::from_webc(&container, &rt)
683 .await
684 .unwrap();
685
686 let mut envb = envb
687 .prepare_webc_env(
688 "test",
689 &annotations,
690 PackageOrHash::Package(&binpkg),
691 RuntimeOrEngine::Runtime(Arc::new(rt)),
692 None,
693 )
694 .unwrap();
695
696 envb = envb.preopen_dir("/host").unwrap();
697
698 let init = envb.build_init().unwrap();
699
700 let fs = &init.state.fs.root_fs;
701
702 fs.read_dir(std::path::Path::new("/host")).unwrap();
703 fs.read_dir(std::path::Path::new("/settings")).unwrap();
704 }
705}