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 Some(Arc::clone(&pkg.webc_fs))
295 }
296 PackageOrHash::Hash(hash) => {
297 builder.set_module_hash(hash);
298 None
299 }
300 };
301
302 if self.wasi.is_home_mapped {
303 builder.set_current_dir(MAPPED_CURRENT_DIR_DEFAULT_PATH);
304 }
305
306 if let Some(current_dir) = &self.wasi.current_dir {
307 builder.set_current_dir(current_dir.clone());
308 }
309
310 if let Some(cwd) = &wasi.cwd {
311 builder.set_current_dir(cwd);
312 }
313
314 self.wasi
315 .prepare_webc_env(&mut builder, container_fs, wasi, root_fs)?;
316
317 if let Some(stdin) = &self.stdin {
318 builder.set_stdin(Box::new(stdin.clone()));
319 }
320 if let Some(stdout) = &self.stdout {
321 builder.set_stdout(Box::new(stdout.clone()));
322 }
323 if let Some(stderr) = &self.stderr {
324 builder.set_stderr(Box::new(stderr.clone()));
325 }
326
327 Ok(builder)
328 }
329
330 pub fn run_wasm(
331 &self,
332 runtime_or_engine: RuntimeOrEngine,
333 program_name: &str,
334 module: Module,
335 module_hash: ModuleHash,
336 ) -> Result<(), Error> {
337 let tokio_runtime = Self::ensure_tokio_runtime();
339 let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
340
341 let wasi = webc::metadata::annotations::Wasi::new(program_name);
342
343 let mut builder = self.prepare_webc_env(
344 program_name,
345 &wasi,
346 PackageOrHash::Hash(module_hash),
347 runtime_or_engine,
348 None,
349 )?;
350
351 #[cfg(feature = "ctrlc")]
352 {
353 builder = builder.attach_ctrl_c();
354 }
355
356 #[cfg(feature = "journal")]
357 {
358 for journal in self.wasi.read_only_journals.iter().cloned() {
359 builder.add_read_only_journal(journal);
360 }
361 for journal in self.wasi.writable_journals.iter().cloned() {
362 builder.add_writable_journal(journal);
363 }
364
365 if !self.wasi.snapshot_on.is_empty() {
366 for trigger in self.wasi.snapshot_on.iter().cloned() {
367 builder.add_snapshot_trigger(trigger);
368 }
369 } else if !self.wasi.writable_journals.is_empty() {
370 for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
371 builder.add_snapshot_trigger(on);
372 }
373 }
374
375 if let Some(period) = self.wasi.snapshot_interval {
376 if self.wasi.writable_journals.is_empty() {
377 return Err(anyhow::format_err!(
378 "If you specify a snapshot interval then you must also specify a writable journal file"
379 ));
380 }
381 builder.with_snapshot_interval(period);
382 }
383
384 builder.with_stop_running_after_snapshot(self.wasi.stop_running_after_snapshot);
385 builder.with_skip_stdio_during_bootstrap(self.wasi.skip_stdio_during_bootstrap);
386 }
387
388 let env = builder.build()?;
389 let runtime = env.runtime.clone();
390 let tasks = runtime.task_manager().clone();
391
392 let mut task_handle =
393 crate::bin_factory::spawn_exec_module(module, env, &runtime).context("Spawn failed")?;
394
395 #[cfg(feature = "ctrlc")]
396 task_handle.install_ctrlc_handler();
397 let task_handle = async move { task_handle.wait_finished().await }.in_current_span();
398
399 let result = tasks.spawn_and_block_on(task_handle)?;
400 let exit_code = result
401 .map_err(|err| {
402 let msg = err.to_string();
404 let weak = Arc::downgrade(&err);
405 Arc::into_inner(err).unwrap_or_else(|| {
406 weak.upgrade()
407 .map(|err| wasi_runtime_error_to_owned(&err))
408 .unwrap_or_else(|| {
409 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
410 })
411 })
412 })
413 .context("Unable to wait for the process to exit")?;
414
415 if exit_code.raw() == 0 {
416 Ok(())
417 } else {
418 Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
419 }
420 }
421
422 pub fn run_command(
423 &mut self,
424 command_name: &str,
425 pkg: &BinaryPackage,
426 runtime_or_engine: RuntimeOrEngine,
427 ) -> Result<(), Error> {
428 let tokio_runtime = Self::ensure_tokio_runtime();
430 let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
431
432 let cmd = pkg
433 .get_command(command_name)
434 .with_context(|| format!("The package doesn't contain a \"{command_name}\" command"))?;
435 let wasi = cmd
436 .metadata()
437 .annotation("wasi")?
438 .unwrap_or_else(|| Wasi::new(command_name));
439
440 let exec_name = if let Some(exec_name) = wasi.exec_name.as_ref() {
441 exec_name
442 } else {
443 command_name
444 };
445
446 #[allow(unused_mut)]
447 let mut builder = self
448 .prepare_webc_env(
449 exec_name,
450 &wasi,
451 PackageOrHash::Package(pkg),
452 runtime_or_engine,
453 None,
454 )
455 .context("Unable to prepare the WASI environment")?;
456
457 #[cfg(feature = "journal")]
458 {
459 for journal in self.wasi.read_only_journals.iter().cloned() {
460 builder.add_read_only_journal(journal);
461 }
462 for journal in self.wasi.writable_journals.iter().cloned() {
463 builder.add_writable_journal(journal);
464 }
465
466 if !self.wasi.snapshot_on.is_empty() {
467 for trigger in self.wasi.snapshot_on.iter().cloned() {
468 builder.add_snapshot_trigger(trigger);
469 }
470 } else if !self.wasi.writable_journals.is_empty() {
471 for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
472 builder.add_snapshot_trigger(on);
473 }
474 }
475
476 if let Some(period) = self.wasi.snapshot_interval {
477 if self.wasi.writable_journals.is_empty() {
478 return Err(anyhow::format_err!(
479 "If you specify a snapshot interval then you must also specify a journal file"
480 ));
481 }
482 builder.with_snapshot_interval(period);
483 }
484
485 builder.with_stop_running_after_snapshot(self.wasi.stop_running_after_snapshot);
486 }
487
488 let env = builder.build()?;
489 let runtime = env.runtime.clone();
490 let command_name = command_name.to_string();
491 let tasks = runtime.task_manager().clone();
492 let pkg = pkg.clone();
493
494 let exit_code = tasks.spawn_and_block_on(
499 async move {
500 let mut task_handle =
501 crate::bin_factory::spawn_exec(pkg, &command_name, env, &runtime)
502 .await
503 .context("Spawn failed")?;
504
505 #[cfg(feature = "ctrlc")]
506 task_handle.install_ctrlc_handler();
507
508 task_handle
509 .wait_finished()
510 .await
511 .map_err(|err| {
512 let msg = err.to_string();
514 let weak = Arc::downgrade(&err);
515 Arc::into_inner(err).unwrap_or_else(|| {
516 weak.upgrade()
517 .map(|err| wasi_runtime_error_to_owned(&err))
518 .unwrap_or_else(|| {
519 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
520 })
521 })
522 })
523 .context("Unable to wait for the process to exit")
524 }
525 .in_current_span(),
526 )??;
527
528 if exit_code.raw() == 0 {
529 Ok(())
530 } else {
531 Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
532 }
533 }
534}
535
536impl crate::runners::Runner for WasiRunner {
537 fn can_run_command(command: &Command) -> Result<bool, Error> {
538 Ok(command
539 .runner
540 .starts_with(webc::metadata::annotations::WASI_RUNNER_URI))
541 }
542
543 #[tracing::instrument(skip_all)]
544 fn run_command(
545 &mut self,
546 command_name: &str,
547 pkg: &BinaryPackage,
548 runtime: Arc<dyn Runtime + Send + Sync>,
549 ) -> Result<(), Error> {
550 self.run_command(command_name, pkg, RuntimeOrEngine::Runtime(runtime))
551 }
552}
553
554fn wasi_runtime_error_to_owned(err: &WasiRuntimeError) -> WasiRuntimeError {
555 match err {
556 WasiRuntimeError::Init(a) => WasiRuntimeError::Init(a.clone()),
557 WasiRuntimeError::Export(a) => WasiRuntimeError::Export(a.clone()),
558 WasiRuntimeError::Instantiation(a) => WasiRuntimeError::Instantiation(a.clone()),
559 WasiRuntimeError::Wasi(WasiError::Exit(a)) => WasiRuntimeError::Wasi(WasiError::Exit(*a)),
560 WasiRuntimeError::Wasi(WasiError::ThreadExit) => {
561 WasiRuntimeError::Wasi(WasiError::ThreadExit)
562 }
563 WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion) => {
564 WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion)
565 }
566 WasiRuntimeError::Wasi(WasiError::DeepSleep(_)) => {
567 WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("deep-sleep")))
568 }
569 WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol)) => {
570 WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol.clone()))
571 }
572 WasiRuntimeError::ControlPlane(a) => WasiRuntimeError::ControlPlane(a.clone()),
573 WasiRuntimeError::Runtime(a) => WasiRuntimeError::Runtime(a.clone()),
574 WasiRuntimeError::Thread(a) => WasiRuntimeError::Thread(a.clone()),
575 WasiRuntimeError::Anyhow(a) => WasiRuntimeError::Anyhow(a.clone()),
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn send_and_sync() {
585 fn assert_send<T: Send>() {}
586 fn assert_sync<T: Sync>() {}
587
588 assert_send::<WasiRunner>();
589 assert_sync::<WasiRunner>();
590 }
591
592 #[cfg(all(feature = "host-fs", feature = "sys"))]
593 #[tokio::test]
594 async fn test_volume_mount_without_webcs() {
595 use std::sync::Arc;
596
597 let root_fs = virtual_fs::RootFileSystemBuilder::new().build();
598
599 let tokrt = tokio::runtime::Handle::current();
600
601 let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
602 let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
603
604 root_fs
605 .mount("/host".into(), &hostdir_dyn, "/".into())
606 .unwrap();
607
608 let envb = crate::runners::wasi::WasiRunner::new();
609
610 let annotations = webc::metadata::annotations::Wasi::new("test");
611
612 let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
613 tokrt.clone(),
614 ));
615 let rt = crate::PluggableRuntime::new(tm);
616
617 let envb = envb
618 .prepare_webc_env(
619 "test",
620 &annotations,
621 PackageOrHash::Hash(ModuleHash::random()),
622 RuntimeOrEngine::Runtime(Arc::new(rt)),
623 Some(root_fs),
624 )
625 .unwrap();
626
627 let init = envb.build_init().unwrap();
628
629 let fs = &init.state.fs.root_fs;
630
631 fs.read_dir(std::path::Path::new("/host")).unwrap();
632 }
633
634 #[cfg(all(feature = "host-fs", feature = "sys"))]
635 #[tokio::test]
636 async fn test_volume_mount_with_webcs() {
637 use std::sync::Arc;
638
639 use wasmer_package::utils::from_bytes;
640
641 let root_fs = virtual_fs::RootFileSystemBuilder::new().build();
642
643 let tokrt = tokio::runtime::Handle::current();
644
645 let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
646 let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
647
648 root_fs
649 .mount("/host".into(), &hostdir_dyn, "/".into())
650 .unwrap();
651
652 let envb = crate::runners::wasi::WasiRunner::new();
653
654 let annotations = webc::metadata::annotations::Wasi::new("test");
655
656 let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
657 tokrt.clone(),
658 ));
659 let mut rt = crate::PluggableRuntime::new(tm);
660 rt.set_package_loader(crate::runtime::package_loader::BuiltinPackageLoader::new());
661
662 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");
663 let webc_data = std::fs::read(webc_path).unwrap();
664 let container = from_bytes(webc_data).unwrap();
665
666 let binpkg = crate::bin_factory::BinaryPackage::from_webc(&container, &rt)
667 .await
668 .unwrap();
669
670 let mut envb = envb
671 .prepare_webc_env(
672 "test",
673 &annotations,
674 PackageOrHash::Package(&binpkg),
675 RuntimeOrEngine::Runtime(Arc::new(rt)),
676 Some(root_fs),
677 )
678 .unwrap();
679
680 envb = envb.preopen_dir("/host").unwrap();
681
682 let init = envb.build_init().unwrap();
683
684 let fs = &init.state.fs.root_fs;
685
686 fs.read_dir(std::path::Path::new("/host")).unwrap();
687 fs.read_dir(std::path::Path::new("/settings")).unwrap();
688 }
689}