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