1use std::collections::HashMap;
2use std::env;
3use std::path::{Path, PathBuf};
4use std::sync::mpsc;
5use std::time::{Duration, Instant};
6
7use mimobox_core::{
8 DirEntry, FileStat, Sandbox, SandboxConfig, SandboxError, SandboxResult, SandboxSnapshot,
9};
10use tracing::debug;
11
12use crate::http_proxy::{HttpProxyError, HttpRequest, HttpResponse};
13use crate::snapshot::MicrovmSnapshot;
14#[cfg(all(target_os = "linux", feature = "kvm"))]
15use crate::snapshot::load_state_from_memory_file;
16
17#[cfg(all(target_os = "linux", feature = "kvm", not(feature = "zerocopy-fork")))]
18use crate::snapshot::{
19 FILE_SNAPSHOT_VERSION, SnapshotStateFile, create_snapshot_dir, memory_sha256_hex,
20};
21use crate::vm_assets::resolve_vm_assets_dir;
22
23#[cfg(all(target_os = "linux", feature = "kvm"))]
24use crate::kvm::{KvmBackend, restore_runtime_state};
25
26#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct MicrovmConfig {
33 pub vcpu_count: u8,
35 pub memory_mb: u32,
37 #[serde(default)]
39 pub cpu_quota_us: Option<u64>,
40 pub kernel_path: PathBuf,
42 pub rootfs_path: PathBuf,
44}
45
46impl Default for MicrovmConfig {
47 fn default() -> Self {
48 let assets_dir = resolve_vm_assets_dir(
49 env::var_os("VM_ASSETS_DIR").map(PathBuf::from),
50 env::var_os("HOME").map(PathBuf::from),
51 )
52 .unwrap_or_else(|_| PathBuf::from("/var/lib/mimobox/vm"));
53
54 Self {
55 vcpu_count: 1,
56 memory_mb: 256,
57 cpu_quota_us: None,
58 kernel_path: assets_dir.join("vmlinux"),
59 rootfs_path: assets_dir.join("rootfs.cpio.gz"),
60 }
61 }
62}
63
64impl MicrovmConfig {
65 pub fn memory_bytes(&self) -> Result<usize, MicrovmError> {
70 let bytes = u64::from(self.memory_mb)
71 .checked_mul(1024 * 1024)
72 .ok_or_else(|| {
73 MicrovmError::InvalidConfig("memory_mb overflow when converting to bytes".into())
74 })?;
75 usize::try_from(bytes).map_err(|_| {
76 MicrovmError::InvalidConfig(
77 "required memory size exceeds platform address space".into(),
78 )
79 })
80 }
81
82 pub fn validate(&self) -> Result<(), MicrovmError> {
87 if self.vcpu_count == 0 {
88 return Err(MicrovmError::InvalidConfig(
89 "vcpu_count must not be 0".into(),
90 ));
91 }
92
93 if self.memory_mb < 64 {
94 return Err(MicrovmError::InvalidConfig(
95 "memory_mb must not be less than 64".into(),
96 ));
97 }
98
99 if self.kernel_path.as_os_str().is_empty() {
100 return Err(MicrovmError::InvalidConfig(
101 "kernel_path must not be empty".into(),
102 ));
103 }
104
105 if self.rootfs_path.as_os_str().is_empty() {
106 return Err(MicrovmError::InvalidConfig(
107 "rootfs_path must not be empty".into(),
108 ));
109 }
110
111 if !self.kernel_path.exists() {
112 return Err(MicrovmError::InvalidConfig(format!(
113 "kernel_path does not exist: {}",
114 self.kernel_path.display()
115 )));
116 }
117
118 if !self.rootfs_path.exists() {
119 return Err(MicrovmError::InvalidConfig(format!(
120 "rootfs_path does not exist: {}",
121 self.rootfs_path.display()
122 )));
123 }
124
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum MicrovmState {
132 Created,
134 Ready,
136 Running,
138 Destroyed,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct GuestCommandResult {
145 pub stdout: Vec<u8>,
147 pub stderr: Vec<u8>,
149 pub exit_code: Option<i32>,
151 pub timed_out: bool,
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
157pub struct GuestExecOptions {
158 pub env: HashMap<String, String>,
160 pub timeout: Option<Duration>,
162 pub cwd: Option<String>,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum StreamEvent {
169 Stdout(Vec<u8>),
171 Stderr(Vec<u8>),
173 Exit(i32),
175 TimedOut,
177}
178
179#[derive(Debug, Clone, thiserror::Error)]
184pub enum LifecycleError {
185 #[error("{message}")]
187 InvalidState {
188 expected: String,
190 current: String,
192 message: String,
194 },
195 #[error("{0}")]
197 Destroyed(
198 String,
200 ),
201 #[error("{0}")]
203 Released(
204 String,
206 ),
207 #[error("snapshot only allowed in Ready state")]
209 NotReady,
210 #[error("fork only allowed in Ready state")]
212 NotReadyForFork,
213 #[error("vsock command channel is not connected")]
215 VsockNotConnected,
216 #[error("vsock command channel unavailable")]
218 VsockUnavailable,
219 #[error("{0}")]
221 Other(
222 String,
224 ),
225}
226
227#[derive(Debug, Clone, thiserror::Error)]
229pub enum GuestFileErrorKind {
230 #[error("path not found")]
232 NotFound,
233 #[error("I/O error")]
235 Io,
236 #[error("permission denied")]
238 PermissionDenied,
239 #[error("out of space")]
241 OutOfSpace,
242 #[error("unknown status code {0}")]
244 Unknown(
245 u8,
247 ),
248}
249
250#[derive(Debug, thiserror::Error)]
255pub enum MicrovmError {
256 #[error("KVM microVM backend not supported on current platform")]
258 UnsupportedPlatform,
259
260 #[error("invalid microVM config: {0}")]
262 InvalidConfig(
263 String,
265 ),
266
267 #[error("microVM lifecycle error: {0}")]
269 Lifecycle(
270 LifecycleError,
272 ),
273
274 #[error("KVM backend error: {0}")]
276 Backend(
277 String,
279 ),
280
281 #[error("guest file error: {path}: {kind}")]
283 GuestFile {
284 kind: GuestFileErrorKind,
286 path: String,
288 },
289
290 #[error(transparent)]
292 HttpProxy(
293 #[from]
295 HttpProxyError,
296 ),
297
298 #[error("invalid snapshot format: {0}")]
300 SnapshotFormat(
301 String,
303 ),
304
305 #[error("I/O error: {0}")]
307 Io(
308 #[from]
310 std::io::Error,
311 ),
312}
313
314impl From<MicrovmError> for SandboxError {
315 fn from(value: MicrovmError) -> Self {
316 match value {
317 MicrovmError::UnsupportedPlatform => SandboxError::Unsupported,
318 MicrovmError::InvalidConfig(message)
319 | MicrovmError::Backend(message)
320 | MicrovmError::HttpProxy(crate::http_proxy::HttpProxyError::Internal(message))
321 | MicrovmError::SnapshotFormat(message) => SandboxError::ExecutionFailed(message),
322 MicrovmError::Lifecycle(error) => SandboxError::ExecutionFailed(error.to_string()),
323 error @ MicrovmError::GuestFile { .. } => {
324 SandboxError::ExecutionFailed(error.to_string())
325 }
326 MicrovmError::HttpProxy(error) => SandboxError::ExecutionFailed(error.to_string()),
327 MicrovmError::Io(error) => SandboxError::Io(error),
328 }
329 }
330}
331
332#[allow(dead_code)]
333enum BackendHandle {
334 #[cfg(all(target_os = "linux", feature = "kvm"))]
335 Kvm(Box<KvmBackend>),
336 Unsupported,
337}
338
339impl BackendHandle {
340 fn create(base_config: SandboxConfig, config: MicrovmConfig) -> Result<Self, MicrovmError> {
341 #[cfg(all(target_os = "linux", feature = "kvm"))]
342 {
343 return Ok(Self::Kvm(Box::new(KvmBackend::create_vm(
344 base_config,
345 config,
346 )?)));
347 }
348
349 #[allow(unreachable_code)]
350 {
351 let _ = base_config;
352 let _ = config;
353 Err(MicrovmError::UnsupportedPlatform)
354 }
355 }
356
357 fn create_for_restore(
358 base_config: SandboxConfig,
359 config: MicrovmConfig,
360 ) -> Result<Self, MicrovmError> {
361 #[cfg(all(target_os = "linux", feature = "kvm"))]
362 {
363 return Ok(Self::Kvm(Box::new(KvmBackend::create_vm_for_restore(
364 base_config,
365 config,
366 )?)));
367 }
368
369 #[allow(unreachable_code)]
370 {
371 let _ = base_config;
372 let _ = config;
373 Err(MicrovmError::UnsupportedPlatform)
374 }
375 }
376
377 #[allow(dead_code)]
378 fn run_command(&mut self, _cmd: &[String]) -> Result<GuestCommandResult, MicrovmError> {
379 self.run_command_with_options(_cmd, &GuestExecOptions::default())
380 }
381
382 fn run_command_with_options(
383 &mut self,
384 _cmd: &[String],
385 _options: &GuestExecOptions,
386 ) -> Result<GuestCommandResult, MicrovmError> {
387 match self {
388 #[cfg(all(target_os = "linux", feature = "kvm"))]
389 Self::Kvm(backend) => backend.run_command_with_options(_cmd, _options),
390 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
391 }
392 }
393
394 #[allow(dead_code)]
395 fn run_command_streaming(
396 &mut self,
397 _cmd: &[String],
398 ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
399 self.run_command_streaming_with_options(_cmd, &GuestExecOptions::default())
400 }
401
402 fn run_command_streaming_with_options(
403 &mut self,
404 _cmd: &[String],
405 _options: &GuestExecOptions,
406 ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
407 match self {
408 #[cfg(all(target_os = "linux", feature = "kvm"))]
409 Self::Kvm(backend) => backend.run_command_streaming_with_options(_cmd, _options),
410 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
411 }
412 }
413
414 fn read_file(&mut self, _path: &str) -> Result<Vec<u8>, MicrovmError> {
415 match self {
416 #[cfg(all(target_os = "linux", feature = "kvm"))]
417 Self::Kvm(backend) => backend.read_file(_path),
418 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
419 }
420 }
421
422 fn write_file(&mut self, _path: &str, _data: &[u8]) -> Result<(), MicrovmError> {
423 match self {
424 #[cfg(all(target_os = "linux", feature = "kvm"))]
425 Self::Kvm(backend) => backend.write_file(_path, _data),
426 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
427 }
428 }
429
430 fn ping(&mut self) -> Result<Duration, MicrovmError> {
431 match self {
432 #[cfg(all(target_os = "linux", feature = "kvm"))]
433 Self::Kvm(backend) => backend.ping(),
434 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
435 }
436 }
437
438 fn ping_with_timeout(&mut self, _timeout: Duration) -> Result<Duration, MicrovmError> {
439 match self {
440 #[cfg(all(target_os = "linux", feature = "kvm"))]
441 Self::Kvm(backend) => backend.ping_with_timeout(_timeout),
442 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
443 }
444 }
445
446 fn http_request(&mut self, _request: HttpRequest) -> Result<HttpResponse, MicrovmError> {
447 match self {
448 #[cfg(all(target_os = "linux", feature = "kvm"))]
449 Self::Kvm(backend) => backend.http_request(_request),
450 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
451 }
452 }
453
454 fn shutdown(&mut self) -> Result<(), MicrovmError> {
455 match self {
456 #[cfg(all(target_os = "linux", feature = "kvm"))]
457 Self::Kvm(backend) => backend.shutdown(),
458 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
459 }
460 }
461
462 fn is_destroyed(&self) -> bool {
463 match self {
464 #[cfg(all(target_os = "linux", feature = "kvm"))]
465 Self::Kvm(backend) => backend.lifecycle() == crate::kvm::KvmLifecycle::Destroyed,
466 Self::Unsupported => true,
467 }
468 }
469
470 fn is_ready(&self) -> bool {
471 match self {
472 #[cfg(all(target_os = "linux", feature = "kvm"))]
473 Self::Kvm(backend) => backend.is_guest_ready(),
474 Self::Unsupported => false,
475 }
476 }
477
478 fn snapshot_parts(&self) -> Result<(Vec<u8>, Vec<u8>), MicrovmError> {
479 match self {
480 #[cfg(all(target_os = "linux", feature = "kvm"))]
481 Self::Kvm(backend) => backend.snapshot_state(),
482 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
483 }
484 }
485
486 fn restore_parts(&mut self, _memory: &[u8], _vcpu_state: &[u8]) -> Result<(), MicrovmError> {
487 match self {
488 #[cfg(all(target_os = "linux", feature = "kvm"))]
489 Self::Kvm(backend) => backend.restore_state(_memory, _vcpu_state),
490 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
491 }
492 }
493
494 #[cfg(all(target_os = "linux", feature = "kvm"))]
495 fn restore_from_file_parts(
496 &mut self,
497 memory_path: &Path,
498 vcpu_state: &[u8],
499 ) -> Result<(), MicrovmError> {
500 match self {
501 #[cfg(all(target_os = "linux", feature = "kvm"))]
502 Self::Kvm(backend) => {
503 let mut restore_profile = backend.take_or_seed_restore_profile();
504
505 let restore_memory_started_at = Instant::now();
506 #[cfg(feature = "zerocopy-fork")]
507 backend.restore_from_file_zerocopy(memory_path)?;
508 #[cfg(not(feature = "zerocopy-fork"))]
509 backend.restore_from_file(memory_path)?;
510 restore_profile.memory_state_write = restore_memory_started_at.elapsed();
511
512 restore_profile.cpuid_config = backend.prepare_restored_vcpus()?;
513
514 let runtime_restore_profile = restore_runtime_state(backend, vcpu_state)?;
515 restore_profile.vcpu_state_restore = runtime_restore_profile.vcpu_state_restore;
516 restore_profile.device_state_restore = runtime_restore_profile.device_state_restore;
517
518 backend.set_lifecycle_ready();
519 backend.emit_restore_profile_without_resume(&restore_profile);
520 backend.set_pending_restore_profile(restore_profile);
521 Ok(())
522 }
523 Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
524 }
525 }
526}
527
528pub struct MicrovmSandbox {
534 base_config: SandboxConfig,
535 microvm_config: MicrovmConfig,
536 state: MicrovmState,
537 backend: BackendHandle,
538}
539
540impl std::fmt::Debug for MicrovmSandbox {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 f.debug_struct("MicrovmSandbox")
543 .field("microvm_config", &self.microvm_config)
544 .field("state", &self.state)
545 .finish_non_exhaustive()
546 }
547}
548
549impl MicrovmSandbox {
550 pub fn new(config: MicrovmConfig) -> Result<Self, MicrovmError> {
554 Self::new_with_base(SandboxConfig::default(), config)
555 }
556
557 pub fn new_with_base(
563 base_config: SandboxConfig,
564 microvm_config: MicrovmConfig,
565 ) -> Result<Self, MicrovmError> {
566 if !cfg!(all(target_os = "linux", feature = "kvm")) {
567 return Err(MicrovmError::UnsupportedPlatform);
568 }
569
570 microvm_config.validate()?;
571 debug!(
572 vcpu_count = microvm_config.vcpu_count,
573 memory_mb = microvm_config.memory_mb,
574 "初始化 microVM 沙箱"
575 );
576 let backend = BackendHandle::create(base_config.clone(), microvm_config.clone())?;
577
578 Ok(Self {
579 base_config,
580 microvm_config,
581 state: MicrovmState::Ready,
582 backend,
583 })
584 }
585
586 pub fn snapshot(&mut self) -> Result<SandboxSnapshot, MicrovmError> {
592 if self.state != MicrovmState::Ready {
593 return Err(MicrovmError::Lifecycle(LifecycleError::NotReady));
594 }
595
596 let (memory, vcpu_state) = self.backend.snapshot_parts()?;
597 MicrovmSnapshot::new(
598 self.base_config.clone(),
599 self.microvm_config.clone(),
600 memory,
601 vcpu_state,
602 )
603 .persist_to_files()
604 }
605
606 pub fn restore(snapshot: &SandboxSnapshot) -> Result<Self, MicrovmError> {
611 let _span = tracing::info_span!("vm_restore").entered();
612 if let Some(memory_path) = snapshot.memory_file_path() {
613 return Self::restore_from_file_snapshot(memory_path);
614 }
615
616 let data = snapshot.as_bytes().map_err(map_snapshot_access_error)?;
617 Self::restore_from_bytes(data)
618 }
619
620 pub fn restore_from_bytes(data: &[u8]) -> Result<Self, MicrovmError> {
624 let snapshot = MicrovmSnapshot::restore(data)?;
625 Self::from_snapshot(snapshot)
626 }
627
628 #[cfg(all(target_os = "linux", feature = "kvm"))]
630 fn restore_from_file_snapshot(memory_path: &Path) -> Result<Self, MicrovmError> {
631 let (sandbox_config, microvm_config, vcpu_state) =
632 load_state_from_memory_file(memory_path)?;
633
634 let mut backend =
635 BackendHandle::create_for_restore(sandbox_config.clone(), microvm_config.clone())?;
636 backend.restore_from_file_parts(memory_path, &vcpu_state)?;
637
638 Ok(Self {
639 base_config: sandbox_config,
640 microvm_config,
641 state: MicrovmState::Ready,
642 backend,
643 })
644 }
645
646 #[cfg(not(all(target_os = "linux", feature = "kvm")))]
648 fn restore_from_file_snapshot(_memory_path: &Path) -> Result<Self, MicrovmError> {
649 Err(MicrovmError::Backend(
650 "file snapshot restore only supported on Linux".into(),
651 ))
652 }
653
654 #[cfg(all(target_os = "linux", feature = "kvm"))]
660 #[cfg_attr(docsrs, doc(cfg(feature = "kvm")))]
661 pub fn fork(&mut self) -> Result<Self, MicrovmError> {
662 let _span = tracing::info_span!("vm_fork").entered();
663 if self.state != MicrovmState::Ready {
664 return Err(MicrovmError::Lifecycle(LifecycleError::NotReadyForFork));
665 }
666
667 #[cfg(feature = "zerocopy-fork")]
668 {
669 let (shared_memory, vcpu_state) = match &self.backend {
670 BackendHandle::Kvm(backend) => backend.snapshot_for_fork()?,
671 BackendHandle::Unsupported => return Err(MicrovmError::UnsupportedPlatform),
672 };
673
674 let mut backend_handle = BackendHandle::create_for_restore(
675 self.base_config.clone(),
676 self.microvm_config.clone(),
677 )?;
678
679 match &mut backend_handle {
680 BackendHandle::Kvm(backend) => {
681 backend.restore_from_shared_memory(shared_memory, &vcpu_state)?;
682 }
683 BackendHandle::Unsupported => return Err(MicrovmError::UnsupportedPlatform),
684 }
685
686 return Ok(Self {
687 base_config: self.base_config.clone(),
688 microvm_config: self.microvm_config.clone(),
689 state: MicrovmState::Ready,
690 backend: backend_handle,
691 });
692 }
693
694 #[cfg(not(feature = "zerocopy-fork"))]
695 {
696 use base64::Engine as _;
697
698 let (memory, vcpu_state) = self.backend.snapshot_parts()?;
699 let snapshot_dir = create_snapshot_dir()?;
700 let memory_path = snapshot_dir.join("memory.bin");
701 let state_path = snapshot_dir.join("state.json");
702
703 let fork_result = (|| {
704 std::fs::write(&memory_path, &memory)?;
705
706 let state = SnapshotStateFile {
707 version: FILE_SNAPSHOT_VERSION,
708 sandbox_config: self.base_config.clone(),
709 microvm_config: self.microvm_config.clone(),
710 vcpu_state_base64: base64::engine::general_purpose::STANDARD
711 .encode(&vcpu_state),
712 memory_hash: Some(memory_sha256_hex(&memory)),
713 };
714 let state_bytes = serde_json::to_vec_pretty(&state).map_err(|error| {
715 MicrovmError::SnapshotFormat(format!("failed to serialize state.json: {error}"))
716 })?;
717 std::fs::write(&state_path, state_bytes)?;
718
719 Self::restore_from_file_snapshot(&memory_path)
720 })();
721
722 let _ = std::fs::remove_dir_all(snapshot_dir);
723 fork_result
724 }
725 }
726
727 #[cfg(not(all(target_os = "linux", feature = "kvm")))]
732 pub fn fork(&mut self) -> Result<Self, MicrovmError> {
733 let _span = tracing::info_span!("vm_fork").entered();
734 Err(MicrovmError::Backend(
735 "fork only supported on Linux + KVM".into(),
736 ))
737 }
738
739 pub(crate) fn from_snapshot(snapshot: MicrovmSnapshot) -> Result<Self, MicrovmError> {
741 let (sandbox_config, microvm_config, memory, vcpu_state) = snapshot.into_parts();
742 let backend =
743 BackendHandle::create_for_restore(sandbox_config.clone(), microvm_config.clone())?;
744 let mut sandbox = Self {
745 base_config: sandbox_config,
746 microvm_config,
747 state: MicrovmState::Ready,
748 backend,
749 };
750 sandbox.backend.restore_parts(&memory, &vcpu_state)?;
751 sandbox.state = MicrovmState::Ready;
752 Ok(sandbox)
753 }
754
755 fn with_ready_state<F, T>(&mut self, op_name: &str, op: F) -> Result<T, MicrovmError>
756 where
757 F: FnOnce(&mut BackendHandle) -> Result<T, MicrovmError>,
758 {
759 if self.state != MicrovmState::Ready {
760 return Err(MicrovmError::Lifecycle(LifecycleError::InvalidState {
761 expected: "Ready".into(),
762 current: format!("not Ready for {op_name}"),
763 message: format!("microVM not ready for {op_name}"),
764 }));
765 }
766
767 self.state = MicrovmState::Running;
768 let result = op(&mut self.backend);
769 self.state = if self.backend.is_destroyed() {
770 MicrovmState::Destroyed
771 } else {
772 MicrovmState::Ready
773 };
774 result
775 }
776
777 pub fn read_file(&mut self, path: &str) -> Result<Vec<u8>, MicrovmError> {
782 self.with_ready_state("read_file", |backend| backend.read_file(path))
783 }
784
785 pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), MicrovmError> {
790 self.with_ready_state("write_file", |backend| backend.write_file(path, data))
791 }
792
793 pub fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>, MicrovmError> {
795 self.with_ready_state("list_dir", |backend| {
796 crate::guest_file_ops::list_dir(path, |cmd| backend.run_command(cmd))
797 })
798 }
799
800 pub fn file_exists(&mut self, path: &str) -> Result<bool, MicrovmError> {
802 self.with_ready_state("file_exists", |backend| {
803 crate::guest_file_ops::file_exists(path, |cmd| backend.run_command(cmd))
804 })
805 }
806
807 pub fn remove_file(&mut self, path: &str) -> Result<(), MicrovmError> {
809 self.with_ready_state("remove_file", |backend| {
810 crate::guest_file_ops::remove_file(path, |cmd| backend.run_command(cmd))
811 })
812 }
813
814 pub fn rename(&mut self, from: &str, to: &str) -> Result<(), MicrovmError> {
816 self.with_ready_state("rename", |backend| {
817 crate::guest_file_ops::rename(from, to, |cmd| backend.run_command(cmd))
818 })
819 }
820
821 pub fn stat(&mut self, path: &str) -> Result<FileStat, MicrovmError> {
823 self.with_ready_state("stat", |backend| {
824 crate::guest_file_ops::stat(path, |cmd| backend.run_command(cmd))
825 })
826 }
827
828 pub fn wait_ready(&mut self, timeout: Duration) -> Result<(), MicrovmError> {
833 if timeout.is_zero() {
834 return Err(MicrovmError::InvalidConfig(
835 "wait_ready timeout must not be zero".into(),
836 ));
837 }
838 if self.state == MicrovmState::Destroyed {
839 return Err(MicrovmError::Lifecycle(LifecycleError::Destroyed(
840 "microVM destroyed, cannot wait for ready".into(),
841 )));
842 }
843
844 self.with_ready_state("wait_ready", |backend| {
845 backend.ping_with_timeout(timeout).map(|_| ())
846 })
847 }
848
849 pub fn is_ready(&self) -> bool {
851 self.state == MicrovmState::Ready && self.backend.is_ready()
852 }
853
854 pub fn ping(&mut self) -> Result<Duration, MicrovmError> {
858 self.with_ready_state("ping", BackendHandle::ping)
859 }
860
861 pub fn stream_execute(
863 &mut self,
864 cmd: &[String],
865 ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
866 self.stream_execute_with_options(cmd, GuestExecOptions::default())
867 }
868
869 pub fn stream_execute_with_options(
873 &mut self,
874 cmd: &[String],
875 options: GuestExecOptions,
876 ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
877 if cmd.is_empty() {
878 return Err(MicrovmError::InvalidConfig(
879 "command must not be empty".into(),
880 ));
881 }
882
883 self.with_ready_state("stream_execute", |backend| {
884 backend.run_command_streaming_with_options(cmd, &options)
885 })
886 }
887
888 pub fn execute_with_env(
892 &mut self,
893 cmd: &[String],
894 env: HashMap<String, String>,
895 ) -> Result<GuestCommandResult, MicrovmError> {
896 self.execute_with_options(
897 cmd,
898 GuestExecOptions {
899 env,
900 timeout: None,
901 cwd: None,
902 },
903 )
904 }
905
906 pub fn execute_with_timeout(
910 &mut self,
911 cmd: &[String],
912 timeout: Duration,
913 ) -> Result<GuestCommandResult, MicrovmError> {
914 self.execute_with_options(
915 cmd,
916 GuestExecOptions {
917 env: HashMap::new(),
918 timeout: Some(timeout),
919 cwd: None,
920 },
921 )
922 }
923
924 pub fn execute_with_options(
929 &mut self,
930 cmd: &[String],
931 options: GuestExecOptions,
932 ) -> Result<GuestCommandResult, MicrovmError> {
933 if cmd.is_empty() {
934 return Err(MicrovmError::InvalidConfig(
935 "command must not be empty".into(),
936 ));
937 }
938
939 self.with_ready_state("execute", |backend| {
940 backend.run_command_with_options(cmd, &options)
941 })
942 }
943
944 pub fn http_request(&mut self, request: HttpRequest) -> Result<HttpResponse, MicrovmError> {
949 self.with_ready_state("http_request", |backend| backend.http_request(request))
950 }
951}
952
953impl Sandbox for MicrovmSandbox {
954 fn new(config: SandboxConfig) -> Result<Self, SandboxError> {
955 Self::new_with_base(config, MicrovmConfig::default()).map_err(Into::into)
956 }
957
958 fn execute(&mut self, cmd: &[String]) -> Result<SandboxResult, SandboxError> {
959 self.execute_with_options_for_sandbox(cmd, GuestExecOptions::default())
960 .map_err(SandboxError::from)
961 }
962
963 fn create_pty(
964 &mut self,
965 _config: mimobox_core::PtyConfig,
966 ) -> Result<Box<dyn mimobox_core::PtySession>, SandboxError> {
967 Err(SandboxError::UnsupportedOperation(
968 "PTY sessions currently only support OS-level backend, microVM not supported"
969 .to_string(),
970 ))
971 }
972
973 fn read_file(&mut self, path: &str) -> Result<Vec<u8>, SandboxError> {
974 MicrovmSandbox::read_file(self, path).map_err(SandboxError::from)
975 }
976
977 fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), SandboxError> {
978 MicrovmSandbox::write_file(self, path, data).map_err(SandboxError::from)
979 }
980
981 fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>, SandboxError> {
982 MicrovmSandbox::list_dir(self, path).map_err(SandboxError::from)
983 }
984
985 fn file_exists(&mut self, path: &str) -> Result<bool, SandboxError> {
986 MicrovmSandbox::file_exists(self, path).map_err(SandboxError::from)
987 }
988
989 fn remove_file(&mut self, path: &str) -> Result<(), SandboxError> {
990 MicrovmSandbox::remove_file(self, path).map_err(SandboxError::from)
991 }
992
993 fn rename(&mut self, from: &str, to: &str) -> Result<(), SandboxError> {
994 MicrovmSandbox::rename(self, from, to).map_err(SandboxError::from)
995 }
996
997 fn stat(&mut self, path: &str) -> Result<FileStat, SandboxError> {
998 MicrovmSandbox::stat(self, path).map_err(SandboxError::from)
999 }
1000
1001 fn snapshot(&mut self) -> Result<SandboxSnapshot, SandboxError> {
1002 MicrovmSandbox::snapshot(self).map_err(SandboxError::from)
1003 }
1004
1005 fn fork(&mut self) -> Result<Self, SandboxError> {
1006 MicrovmSandbox::fork(self).map_err(SandboxError::from)
1007 }
1008
1009 fn destroy(self) -> Result<(), SandboxError> {
1010 let mut this = self;
1011 this.backend.shutdown().map_err(SandboxError::from)?;
1012 this.state = MicrovmState::Destroyed;
1013 Ok(())
1014 }
1015}
1016
1017fn map_snapshot_access_error(error: SandboxError) -> MicrovmError {
1018 match error {
1019 SandboxError::Io(error) => MicrovmError::Io(error),
1020 SandboxError::InvalidSnapshot => {
1021 MicrovmError::SnapshotFormat("invalid sandbox snapshot".into())
1022 }
1023 other => MicrovmError::SnapshotFormat(other.to_string()),
1024 }
1025}
1026
1027impl MicrovmSandbox {
1028 fn execute_with_options_for_sandbox(
1029 &mut self,
1030 cmd: &[String],
1031 options: GuestExecOptions,
1032 ) -> Result<SandboxResult, MicrovmError> {
1033 if cmd.is_empty() {
1034 return Err(MicrovmError::InvalidConfig(
1035 "command must not be empty".into(),
1036 ));
1037 }
1038
1039 self.with_ready_state("execute", |backend| {
1040 let start = Instant::now();
1041 let guest = backend.run_command_with_options(cmd, &options)?;
1042 Ok(SandboxResult {
1043 stdout: guest.stdout,
1044 stderr: guest.stderr,
1045 exit_code: guest.exit_code,
1046 elapsed: start.elapsed(),
1047 timed_out: guest.timed_out,
1048 })
1049 })
1050 }
1051}