1#[path = "vm_sandbox.rs"]
4mod sandbox;
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8#[cfg(unix)]
9use std::time::Duration;
10
11use a3s_box_core::{
12 EventEmitter, ExecutionBackend, ExecutionId, ExecutionManagerError, ExecutionManagerResult,
13 ExecutionState, KillOutcome, DEFAULT_SHUTDOWN_TIMEOUT_MS,
14};
15use async_trait::async_trait;
16use dashmap::mapref::entry::Entry;
17use dashmap::DashMap;
18use tokio::sync::Mutex;
19
20use super::resources::ExecutionResourceGuard;
21use super::vm_process::{locate_microvm_process, LocatedProcess};
22#[cfg(target_os = "linux")]
23use super::TransientRegistryAuthBroker;
24use super::{
25 LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation,
26 LocalExecutionTermination,
27};
28use crate::vm::{TERMINAL_EXIT_POLL_INTERVAL, TERMINAL_EXIT_POLL_TIMEOUT};
29use crate::{
30 BoxRecord, ManagedExecutionMetadata, ManagedExecutionOperation, ManagedExecutionState,
31 VmManager,
32};
33
34type SharedVm = Arc<Mutex<VmManager>>;
35
36#[derive(Clone)]
39pub struct VmLocalExecutionBackend {
40 home_dir: PathBuf,
41 managers: Arc<DashMap<String, SharedVm>>,
42 pull_progress_fn: Option<crate::PullProgressFn>,
43 #[cfg(target_os = "linux")]
44 transient_registry_auth: Option<TransientRegistryAuthBroker>,
45}
46
47impl VmLocalExecutionBackend {
48 pub fn new(home_dir: impl Into<PathBuf>) -> Self {
49 Self {
50 home_dir: home_dir.into(),
51 managers: Arc::new(DashMap::new()),
52 pull_progress_fn: None,
53 #[cfg(target_os = "linux")]
54 transient_registry_auth: None,
55 }
56 }
57
58 pub fn with_pull_progress_fn(mut self, pull_progress_fn: crate::PullProgressFn) -> Self {
59 self.pull_progress_fn = Some(pull_progress_fn);
60 self
61 }
62
63 #[cfg(target_os = "linux")]
64 pub(crate) fn with_transient_registry_auth(
65 mut self,
66 broker: TransientRegistryAuthBroker,
67 ) -> Self {
68 self.transient_registry_auth = Some(broker);
69 self
70 }
71
72 pub fn home_dir(&self) -> &Path {
73 &self.home_dir
74 }
75
76 fn metadata<'a>(
77 &self,
78 record: &'a BoxRecord,
79 ) -> ExecutionManagerResult<&'a ManagedExecutionMetadata> {
80 self.metadata_for_route(record, crate::ManagedRuntimeRoute::BoxVm)
81 }
82
83 fn metadata_for_route<'a>(
84 &self,
85 record: &'a BoxRecord,
86 expected_route: crate::ManagedRuntimeRoute,
87 ) -> ExecutionManagerResult<&'a ManagedExecutionMetadata> {
88 uuid::Uuid::parse_str(&record.id).map_err(|error| {
89 ExecutionManagerError::Internal(format!(
90 "managed execution has an invalid internal ID {}: {error}",
91 record.id
92 ))
93 })?;
94 let expected_box_dir = self.home_dir.join("boxes").join(&record.id);
95 if record.box_dir != expected_box_dir {
96 return Err(ExecutionManagerError::Internal(format!(
97 "managed execution {} has an unexpected host directory {}",
98 record.id,
99 record.box_dir.display()
100 )));
101 }
102 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
103 ExecutionManagerError::Internal(format!(
104 "execution {} lost managed lifecycle metadata",
105 record.id
106 ))
107 })?;
108 metadata
109 .validate()
110 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
111 let resolved_route = super::router::resolved_runtime_route(record)?;
112 if resolved_route != expected_route {
113 return Err(ExecutionManagerError::Internal(format!(
114 "managed execution {} is pinned to {:?}, not {:?}",
115 record.id, resolved_route, expected_route
116 )));
117 }
118 if record.isolation != metadata.request.config.isolation {
119 return Err(ExecutionManagerError::Internal(format!(
120 "managed execution {} has inconsistent isolation metadata",
121 record.id
122 )));
123 }
124 Ok(metadata)
125 }
126
127 fn new_manager(&self, record: &BoxRecord) -> ExecutionManagerResult<VmManager> {
128 let metadata = self.metadata(record)?;
129 self.new_manager_from_metadata(record, metadata)
130 }
131
132 pub(super) fn new_oci_preparation_manager(
133 &self,
134 record: &BoxRecord,
135 ) -> ExecutionManagerResult<VmManager> {
136 let metadata = self.metadata_for_route(record, crate::ManagedRuntimeRoute::OciSdk)?;
137 self.new_manager_from_metadata(record, metadata)
138 }
139
140 fn new_manager_from_metadata(
141 &self,
142 record: &BoxRecord,
143 metadata: &ManagedExecutionMetadata,
144 ) -> ExecutionManagerResult<VmManager> {
145 let mut config = metadata.request.config.clone();
146 config.network = record.network_mode.clone();
151 if let Some(shm_size) = metadata.request.policy.shm_size {
152 let has_shared_memory_mount = config
153 .tmpfs
154 .iter()
155 .any(|entry| entry.split(':').next() == Some("/dev/shm"));
156 if !has_shared_memory_mount {
157 config.tmpfs.push(format!("/dev/shm:size={shm_size}"));
158 }
159 }
160 let mut manager = VmManager::with_box_id(config, EventEmitter::new(256), record.id.clone());
161 manager.home_dir = self.home_dir.clone();
162 manager.set_healthcheck_disabled(metadata.request.policy.healthcheck_disabled);
163 if let Some(pull_progress_fn) = self.pull_progress_fn.clone() {
164 manager.set_pull_progress_fn(pull_progress_fn);
165 }
166 manager.anonymous_volumes = record.anonymous_volumes.clone();
167 manager.set_log_config(record.log_config.clone());
168 manager.resolved_execution_plan = Some(metadata.plan.clone());
169 manager.managed_secret_root = metadata.request.policy.managed_secret_root.clone();
170 Ok(manager)
171 }
172
173 #[cfg(target_os = "linux")]
174 fn claim_transient_registry_auth_for_boot(&self, manager: &mut VmManager) {
175 manager.transient_registry_auth = self
176 .transient_registry_auth
177 .as_ref()
178 .and_then(|broker| broker.take(manager.box_id()));
179 }
180
181 fn manager(&self, execution_id: &str) -> Option<SharedVm> {
182 self.managers
183 .get(execution_id)
184 .map(|entry| Arc::clone(entry.value()))
185 }
186
187 fn remove_manager(&self, execution_id: &str, expected: &SharedVm) {
188 if let Entry::Occupied(entry) = self.managers.entry(execution_id.to_string()) {
189 if Arc::ptr_eq(entry.get(), expected) {
190 entry.remove();
191 }
192 }
193 }
194
195 async fn handle_from_manager(
196 &self,
197 record: &BoxRecord,
198 manager: &VmManager,
199 ) -> ExecutionManagerResult<LocalExecutionHandle> {
200 let execution_id = execution_id(record)?;
201 let pid = manager.pid().await.ok_or_else(|| {
202 ExecutionManagerError::Internal(format!(
203 "runtime returned no host PID for {execution_id}"
204 ))
205 })?;
206 let pid_start_time = crate::process::pid_start_time(pid);
207 #[cfg(target_os = "linux")]
208 if pid_start_time.is_none() {
209 return Err(ExecutionManagerError::NotFound(execution_id));
210 }
211 if !crate::process::is_process_alive_with_identity(pid, pid_start_time) {
212 return Err(ExecutionManagerError::NotFound(execution_id));
213 }
214 let exec_socket_path = manager
215 .exec_socket_path()
216 .map(Path::to_path_buf)
217 .ok_or_else(|| {
218 ExecutionManagerError::Internal(format!(
219 "runtime returned no exec socket for {}",
220 record.id
221 ))
222 })?;
223 let anonymous_volumes = if manager.anonymous_volumes().is_empty() {
224 self.anonymous_volumes_for_record(record).await
225 } else {
226 manager.anonymous_volumes().to_vec()
227 };
228 Ok(LocalExecutionHandle {
229 started_at: record.started_at.unwrap_or_else(chrono::Utc::now),
230 pid: Some(pid),
231 pid_start_time,
232 exec_socket_path,
233 console_log: record.box_dir.join("logs/console.log"),
234 anonymous_volumes,
235 oci_runtime: None,
236 })
237 }
238
239 async fn inspect_registered(
240 &self,
241 record: &BoxRecord,
242 shared: SharedVm,
243 ) -> ExecutionManagerResult<LocalExecutionObservation> {
244 let mut manager = shared.lock().await;
245 let preserve_rootfs = should_force_rootfs_preservation(record)?;
246 let exit_code = manager
247 .try_wait_exit()
248 .await
249 .map_err(|error| runtime_error("inspect", record, error))?;
250 let mut state = manager.state().await;
251 let terminal = exit_code.is_some() || state == crate::BoxState::Stopped;
252 if terminal {
253 return self
254 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, exit_code)
255 .await;
256 }
257
258 if state == crate::BoxState::Created {
259 if manager.has_exited().await {
260 return self
261 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
262 .await;
263 }
264 if !self.promote_if_ready(record, &mut manager).await {
265 return Ok(LocalExecutionObservation {
266 state: ExecutionState::Creating,
267 handle: None,
268 exit_code: None,
269 });
270 }
271 state = manager.state().await;
272 }
273
274 if !manager
275 .health_check()
276 .await
277 .map_err(|error| runtime_error("inspect", record, error))?
278 {
279 return self
280 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
281 .await;
282 }
283
284 if state != crate::BoxState::Ready
285 && state != crate::BoxState::Busy
286 && state != crate::BoxState::Compacting
287 {
288 return Err(ExecutionManagerError::Internal(format!(
289 "runtime manager for {} is in unexpected state {state:?}",
290 record.id
291 )));
292 }
293 if matches!(
294 managed_state(record)?,
295 ManagedExecutionState::Starting | ManagedExecutionState::RestartStarting
296 ) && !exec_endpoint_ready(manager.exec_socket_path()).await
297 {
298 return Ok(LocalExecutionObservation {
299 state: ExecutionState::Creating,
300 handle: None,
301 exit_code: None,
302 });
303 }
304 let visible_state = visible_active_state(record)?;
305 let handle = match self.handle_from_manager(record, &manager).await {
306 Ok(handle) => handle,
307 Err(ExecutionManagerError::NotFound(_)) => {
308 return self
315 .finish_registered_terminal(record, &shared, manager, preserve_rootfs, None)
316 .await;
317 }
318 Err(error) => return Err(error),
319 };
320 Ok(LocalExecutionObservation {
321 state: visible_state,
322 handle: Some(handle),
323 exit_code: None,
324 })
325 }
326
327 async fn finish_registered_terminal(
328 &self,
329 record: &BoxRecord,
330 shared: &SharedVm,
331 mut manager: tokio::sync::MutexGuard<'_, VmManager>,
332 preserve_rootfs: bool,
333 exit_code: Option<i32>,
334 ) -> ExecutionManagerResult<LocalExecutionObservation> {
335 let deadline = tokio::time::Instant::now() + TERMINAL_EXIT_POLL_TIMEOUT;
340 let mut exit_code = manager.exit_code().or(exit_code);
341 while exit_code.is_none() {
342 exit_code = manager
343 .try_wait_exit()
344 .await
345 .map_err(|error| runtime_error("collect exit status", record, error))?;
346 if exit_code.is_some() || tokio::time::Instant::now() >= deadline {
347 break;
348 }
349 tokio::time::sleep(TERMINAL_EXIT_POLL_INTERVAL).await;
350 }
351 let exit_code = exit_code.ok_or_else(|| {
352 ExecutionManagerError::Unavailable(format!(
353 "runtime reported execution {} as terminal before its exact exit status became available",
354 record.id
355 ))
356 })?;
357 let cleanup = destroy_after_observation(&mut manager, preserve_rootfs).await;
358 drop(manager);
359 self.remove_manager(&record.id, shared);
360 cleanup.map_err(|error| runtime_error("clean up", record, error))?;
361 Ok(LocalExecutionObservation {
362 state: ExecutionState::Stopped,
363 handle: None,
364 exit_code: Some(exit_code),
365 })
366 }
367
368 async fn promote_if_ready(&self, record: &BoxRecord, manager: &mut VmManager) -> bool {
369 let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
370 let exec_socket = socket_dir.join("exec.sock");
371 if !exec_endpoint_ready(Some(&exec_socket)).await {
372 return false;
373 }
374 manager.exec_socket_path = Some(exec_socket);
375 manager.pty_socket_path = Some(socket_dir.join("pty.sock"));
376 manager.port_forward_socket_path = Some(socket_dir.join("portfwd.sock"));
377 *manager.state.write().await = crate::BoxState::Ready;
378 true
379 }
380
381 async fn recover_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
382 self.metadata(record)?;
383 let execution_id = execution_id(record)?;
384 let execution_id_label = record.id.clone();
385 let recorded = record.pid.map(|pid| (pid, record.pid_start_time));
386 let located = tokio::task::spawn_blocking(move || {
387 locate_microvm_process(&execution_id_label, recorded)
388 })
389 .await
390 .map_err(|error| {
391 ExecutionManagerError::Internal(format!(
392 "MicroVM process discovery task failed for {}: {error}",
393 record.id
394 ))
395 })?
396 .map_err(ExecutionManagerError::Internal)?
397 .ok_or(ExecutionManagerError::NotFound(execution_id))?;
398 self.attach_microvm(record, located).await
399 }
400
401 async fn attach_microvm(
402 &self,
403 record: &BoxRecord,
404 located: LocatedProcess,
405 ) -> ExecutionManagerResult<SharedVm> {
406 let mut manager = self.new_manager(record)?;
407 let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
408 manager
409 .attach_running_process(
410 located.pid,
411 socket_dir.join("exec.sock"),
412 Some(socket_dir.join("pty.sock")),
413 )
414 .await
415 .map_err(|error| runtime_error("recover", record, error))?;
416 if located.start_time.is_some()
417 && crate::process::pid_start_time(located.pid) != located.start_time
418 {
419 return Err(ExecutionManagerError::NotFound(execution_id(record)?));
420 }
421 let recovered = Arc::new(Mutex::new(manager));
422 match self.managers.entry(record.id.clone()) {
423 Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
424 Entry::Vacant(entry) => {
425 entry.insert(Arc::clone(&recovered));
426 Ok(recovered)
427 }
428 }
429 }
430
431 #[cfg(not(windows))]
432 async fn require_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
433 match self.manager(&record.id) {
434 Some(manager) => Ok(manager),
435 None => self.recover_microvm(record).await,
436 }
437 }
438
439 async fn destroy_registered(
440 &self,
441 record: &BoxRecord,
442 shared: SharedVm,
443 remove_anonymous_volumes: bool,
444 force_preserve_rootfs: bool,
445 timeout_secs: Option<u64>,
446 ) -> ExecutionManagerResult<LocalExecutionTermination> {
447 let mut manager = shared.lock().await;
448 let mut anonymous_volumes = if manager.anonymous_volumes().is_empty() {
449 record.anonymous_volumes.clone()
450 } else {
451 manager.anonymous_volumes().to_vec()
452 };
453 let result = match (
454 graceful_stop_options(record, timeout_secs)?,
455 force_preserve_rootfs,
456 ) {
457 (Some((signal, timeout_ms)), true) => {
458 manager
459 .destroy_preserving_rootfs_with_options(signal, timeout_ms)
460 .await
461 }
462 (Some((signal, timeout_ms)), false) => {
463 manager.destroy_with_options(signal, timeout_ms).await
464 }
465 (None, true) => manager.destroy_preserving_rootfs().await,
466 (None, false) => manager.destroy().await,
467 };
468 let exit_code = manager.exit_code();
469 drop(manager);
470 self.remove_manager(&record.id, &shared);
471 result.map_err(|error| runtime_error("kill", record, error))?;
472 if remove_anonymous_volumes {
473 if anonymous_volumes.is_empty() {
474 anonymous_volumes = self.anonymous_volumes_for_record(record).await;
475 }
476 self.cleanup_anonymous_volumes(anonymous_volumes).await;
477 }
478 Ok(LocalExecutionTermination {
479 outcome: KillOutcome::Killed,
480 exit_code,
481 })
482 }
483
484 async fn anonymous_volumes_for_record(&self, record: &BoxRecord) -> Vec<String> {
485 if !record.anonymous_volumes.is_empty() {
486 return record.anonymous_volumes.clone();
487 }
488 let home_dir = self.home_dir.clone();
489 let execution_id = record.id.clone();
490 let short_id = record.id.chars().take(8).collect::<String>();
491 let result = tokio::task::spawn_blocking(move || -> a3s_box_core::Result<Vec<String>> {
492 let store =
493 crate::VolumeStore::new(home_dir.join("volumes.json"), home_dir.join("volumes"));
494 let prefix = format!("anon_{short_id}_");
495 let mut names = store
496 .load()?
497 .into_values()
498 .filter(|volume| {
499 volume
500 .labels
501 .get("anonymous")
502 .is_some_and(|value| value == "true")
503 && (volume.in_use_by.iter().any(|id| id == &execution_id)
504 || volume.name.starts_with(&prefix))
505 })
506 .map(|volume| volume.name)
507 .collect::<Vec<_>>();
508 names.sort();
509 Ok(names)
510 })
511 .await;
512 match result {
513 Ok(Ok(names)) => names,
514 Ok(Err(error)) => {
515 tracing::warn!(
516 execution_id = %record.id,
517 %error,
518 "Failed to load anonymous volumes during managed cleanup"
519 );
520 Vec::new()
521 }
522 Err(error) => {
523 tracing::warn!(
524 execution_id = %record.id,
525 %error,
526 "Anonymous volume recovery task failed"
527 );
528 Vec::new()
529 }
530 }
531 }
532
533 async fn cleanup_anonymous_volumes(&self, names: Vec<String>) {
534 if names.is_empty() {
535 return;
536 }
537 let home_dir = self.home_dir.clone();
538 let task = tokio::task::spawn_blocking(move || {
539 let store = crate::VolumeStore::new(
540 home_dir.join("volumes.json"),
541 home_dir.join("volumes"),
542 );
543 for name in names {
544 if let Err(error) = store.remove(&name, true) {
545 tracing::warn!(volume = %name, %error, "Failed to remove managed anonymous volume");
546 }
547 }
548 })
549 .await;
550 if let Err(error) = task {
551 tracing::warn!(%error, "Anonymous volume cleanup task failed");
552 }
553 }
554
555 async fn terminate_execution(
556 &self,
557 record: &BoxRecord,
558 ) -> ExecutionManagerResult<LocalExecutionTermination> {
559 let metadata = self.metadata(record)?;
560 let remove_anonymous_volumes = record.auto_remove;
561 let timeout_secs = record.stop_timeout;
562 if let Some(manager) = self.manager(&record.id) {
563 return self
564 .destroy_registered(
565 record,
566 manager,
567 remove_anonymous_volumes,
568 false,
569 timeout_secs,
570 )
571 .await;
572 }
573 if !metadata.paused_with_memory {
577 let manager = Arc::new(Mutex::new(self.new_manager(record)?));
578 return self
579 .destroy_registered(
580 record,
581 manager,
582 remove_anonymous_volumes,
583 false,
584 timeout_secs,
585 )
586 .await;
587 }
588 match metadata.plan.backend {
589 ExecutionBackend::A3sOci => {
590 self.destroy_detached_sandbox(record, remove_anonymous_volumes, false, timeout_secs)
591 .await
592 }
593 ExecutionBackend::Krun => {
594 let manager = self.recover_microvm(record).await?;
595 self.destroy_registered(
596 record,
597 manager,
598 remove_anonymous_volumes,
599 false,
600 timeout_secs,
601 )
602 .await
603 }
604 }
605 }
606}
607
608async fn destroy_after_observation(
609 manager: &mut VmManager,
610 preserve_rootfs: bool,
611) -> a3s_box_core::Result<()> {
612 if preserve_rootfs {
613 manager.destroy_preserving_rootfs().await
614 } else {
615 manager.destroy().await
616 }
617}
618
619#[async_trait]
620impl LocalExecutionBackend for VmLocalExecutionBackend {
621 fn route_for_create(
622 &self,
623 _record: &BoxRecord,
624 ) -> ExecutionManagerResult<crate::ManagedRuntimeRoute> {
625 Ok(crate::ManagedRuntimeRoute::BoxVm)
626 }
627
628 async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
629 super::record::validate_record_health(record)?;
630 self.metadata(record)?;
631 let box_dir = record.box_dir.clone();
632 let execution_id = record.id.clone();
633 tokio::task::spawn_blocking(move || {
634 crate::rootfs::stage_box_terminal_rootfs_metadata(&box_dir)
635 })
636 .await
637 .map_err(|error| {
638 ExecutionManagerError::Internal(format!(
639 "rootfs metadata staging task failed for {execution_id}: {error}"
640 ))
641 })?
642 .map_err(|error| {
643 ExecutionManagerError::Internal(format!(
644 "failed to stage rootfs metadata for {execution_id}: {error}"
645 ))
646 })?;
647 let mut manager = self.new_manager(record)?;
648 let requested_persistence = manager.config.persistent;
649 if should_reuse_preserved_rootfs(record)?
650 && crate::vm::persistent_rootfs_generation_exists(&record.box_dir)
651 .map_err(|error| runtime_error("inspect retained rootfs", record, error))?
652 {
653 manager.config.persistent = true;
654 }
655 let manager = Arc::new(Mutex::new(manager));
656 match self.managers.entry(record.id.clone()) {
657 Entry::Occupied(_) => {
658 return Err(ExecutionManagerError::Unavailable(format!(
659 "execution {} already has an in-process runtime owner",
660 record.id
661 )))
662 }
663 Entry::Vacant(entry) => {
664 entry.insert(Arc::clone(&manager));
665 }
666 }
667
668 let mut guard = manager.lock().await;
669 let resource_home = self.home_dir.clone();
670 let resource_record = record.clone();
671 let resources = match tokio::task::spawn_blocking(move || {
672 ExecutionResourceGuard::prepare(&resource_home, &resource_record)
673 })
674 .await
675 {
676 Ok(Ok(resources)) => resources,
677 Ok(Err(error)) => {
678 drop(guard);
679 self.remove_manager(&record.id, &manager);
680 return Err(error);
681 }
682 Err(error) => {
683 drop(guard);
684 self.remove_manager(&record.id, &manager);
685 return Err(ExecutionManagerError::Internal(format!(
686 "managed resource preparation task failed for {}: {error}",
687 record.id
688 )));
689 }
690 };
691 #[cfg(target_os = "linux")]
692 self.claim_transient_registry_auth_for_boot(&mut guard);
693 if let Err(error) = guard.boot().await {
694 guard.config.persistent = requested_persistence;
695 if guard.exit_code().is_some() {
702 tracing::debug!(
703 execution_id = %record.id,
704 %error,
705 "Runtime completed while startup was establishing readiness"
706 );
707 resources.disarm();
708 return Err(ExecutionManagerError::Unavailable(format!(
709 "execution {} completed during startup",
710 record.id
711 )));
712 }
713 drop(guard);
714 self.remove_manager(&record.id, &manager);
715 let rollback = tokio::task::spawn_blocking(move || resources.rollback()).await;
716 if let Err(rollback_error) = rollback {
717 tracing::warn!(
718 execution_id = %record.id,
719 %rollback_error,
720 "Managed resource rollback task failed"
721 );
722 }
723 return Err(runtime_error("start", record, error));
724 }
725 guard.config.persistent = requested_persistence;
726 resources.disarm();
727 let exited_during_start = guard
728 .try_wait_exit()
729 .await
730 .map_err(|error| runtime_error("collect startup exit status", record, error))?
731 .is_some()
732 || guard.has_exited().await;
733 if exited_during_start {
734 return Err(ExecutionManagerError::Unavailable(format!(
735 "execution {} completed during startup",
736 record.id
737 )));
738 }
739 self.handle_from_manager(record, &guard).await
740 }
741
742 async fn inspect(
743 &self,
744 record: &BoxRecord,
745 ) -> ExecutionManagerResult<LocalExecutionObservation> {
746 let metadata = self.metadata(record)?;
747 if metadata.plan.backend.is_sandbox() {
748 return self.inspect_sandbox(record).await;
749 }
750 if let Some(manager) = self.manager(&record.id) {
751 return self.inspect_registered(record, manager).await;
752 }
753 let manager = self.recover_microvm(record).await?;
754 self.inspect_registered(record, manager).await
755 }
756
757 async fn pause(
758 &self,
759 record: &BoxRecord,
760 keep_memory: bool,
761 ) -> ExecutionManagerResult<LocalExecutionHandle> {
762 let metadata = self.metadata(record)?;
763 if metadata.plan.backend.is_sandbox() {
764 if !keep_memory {
765 return Err(unsupported(
766 record,
767 "pause without memory retention",
768 "the Sandbox backend",
769 ));
770 }
771 return self.pause_sandbox(record).await;
772 }
773 if !keep_memory {
774 return Err(unsupported(
775 record,
776 "pause without memory retention",
777 "the local MicroVM backend",
778 ));
779 }
780 #[cfg(windows)]
781 {
782 Err(unsupported(
783 record,
784 "pause",
785 "the local MicroVM backend on Windows",
786 ))
787 }
788 #[cfg(not(windows))]
789 {
790 let shared = self.require_microvm(record).await?;
791 let manager = shared.lock().await;
792 require_recorded_pid(record, &manager).await?;
793 manager
794 .pause()
795 .await
796 .map_err(|error| runtime_error("pause", record, error))?;
797 self.handle_from_manager(record, &manager).await
798 }
799 }
800
801 async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
802 let metadata = self.metadata(record)?;
803 if metadata.plan.backend.is_sandbox() {
804 return self.resume_sandbox(record).await;
805 }
806 #[cfg(windows)]
807 {
808 Err(unsupported(
809 record,
810 "resume",
811 "the local MicroVM backend on Windows",
812 ))
813 }
814 #[cfg(not(windows))]
815 {
816 let shared = self.require_microvm(record).await?;
817 let manager = shared.lock().await;
818 require_recorded_pid(record, &manager).await?;
819 manager
820 .resume()
821 .await
822 .map_err(|error| runtime_error("resume", record, error))?;
823 self.handle_from_manager(record, &manager).await
824 }
825 }
826
827 async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
828 self.new_manager(record)?
829 .prepare_preserved_rootfs()
830 .map(|_| ())
831 .map_err(|error| runtime_error("prepare quiescent rootfs", record, error))
832 }
833
834 async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
835 self.new_manager(record)?
836 .cleanup_preserved_rootfs()
837 .map_err(|error| runtime_error("clean up quiescent rootfs", record, error))
838 }
839
840 async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
841 Ok(self.terminate_execution(record).await?.outcome)
842 }
843
844 async fn kill_with_status(
845 &self,
846 record: &BoxRecord,
847 ) -> ExecutionManagerResult<LocalExecutionTermination> {
848 self.terminate_execution(record).await
849 }
850
851 async fn stop_for_restart(
852 &self,
853 record: &BoxRecord,
854 timeout_secs: Option<u64>,
855 ) -> ExecutionManagerResult<KillOutcome> {
856 let metadata = self.metadata(record)?;
857 #[cfg(target_os = "linux")]
858 if metadata.plan.backend.is_sandbox() {
859 super::snapshot::persist_sandbox_snapshot_mappings(record)?;
860 }
861 let timeout_secs = timeout_secs.or(record.stop_timeout);
862 if let Some(manager) = self.manager(&record.id) {
863 return Ok(self
864 .destroy_registered(record, manager, false, true, timeout_secs)
865 .await?
866 .outcome);
867 }
868 match metadata.plan.backend {
869 ExecutionBackend::A3sOci => Ok(self
870 .destroy_detached_sandbox(record, false, true, timeout_secs)
871 .await?
872 .outcome),
873 ExecutionBackend::Krun => {
874 let manager = self.recover_microvm(record).await?;
875 Ok(self
876 .destroy_registered(record, manager, false, true, timeout_secs)
877 .await?
878 .outcome)
879 }
880 }
881 }
882}
883
884pub(super) fn should_force_rootfs_preservation(record: &BoxRecord) -> ExecutionManagerResult<bool> {
885 let state = super::support::managed_state(record)?;
886 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
887 ExecutionManagerError::Internal(format!(
888 "execution {} has no managed lifecycle metadata",
889 record.id
890 ))
891 })?;
892 Ok(match state {
893 ManagedExecutionState::Starting => true,
899 ManagedExecutionState::Pausing => matches!(
900 metadata.pending_operation.as_ref(),
901 Some(ManagedExecutionOperation::Pause {
902 keep_memory: false,
903 ..
904 })
905 ),
906 ManagedExecutionState::Resuming => !metadata.paused_with_memory,
907 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => true,
908 _ => false,
909 })
910}
911
912fn should_reuse_preserved_rootfs(record: &BoxRecord) -> ExecutionManagerResult<bool> {
913 Ok(matches!(
914 super::support::managed_state(record)?,
915 ManagedExecutionState::Resuming | ManagedExecutionState::RestartStarting
916 ) && should_force_rootfs_preservation(record)?)
917}
918
919fn graceful_stop_options(
920 record: &BoxRecord,
921 timeout_secs: Option<u64>,
922) -> ExecutionManagerResult<Option<(i32, u64)>> {
923 if timeout_secs.is_none() && record.stop_signal.is_none() {
924 return Ok(None);
925 }
926 let timeout_ms = timeout_secs
927 .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT_MS / 1_000)
928 .checked_mul(1_000)
929 .ok_or_else(|| {
930 ExecutionManagerError::InvalidRequest(format!(
931 "stop timeout is too large for execution {}",
932 record.id
933 ))
934 })?;
935 let signal = record
936 .stop_signal
937 .as_deref()
938 .map(a3s_box_core::vmm::parse_signal_name)
939 .unwrap_or(libc::SIGTERM);
940 Ok(Some((signal, timeout_ms)))
941}
942
943#[cfg(not(windows))]
944async fn require_recorded_pid(
945 record: &BoxRecord,
946 manager: &VmManager,
947) -> ExecutionManagerResult<()> {
948 let execution_id = execution_id(record)?;
949 let pid = manager
950 .pid()
951 .await
952 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
953 if record.pid != Some(pid)
954 || !crate::process::is_process_alive_with_identity(pid, record.pid_start_time)
955 {
956 return Err(ExecutionManagerError::NotFound(execution_id));
957 }
958 Ok(())
959}
960
961fn visible_active_state(record: &BoxRecord) -> ExecutionManagerResult<ExecutionState> {
962 match managed_state(record)? {
963 ManagedExecutionState::Paused => Ok(ExecutionState::Paused),
964 ManagedExecutionState::Resuming => {
965 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
966 ExecutionManagerError::Internal(format!(
967 "execution {} has no managed lifecycle metadata",
968 record.id
969 ))
970 })?;
971 if metadata.paused_with_memory {
972 Ok(ExecutionState::Paused)
973 } else {
974 Ok(ExecutionState::Running)
978 }
979 }
980 ManagedExecutionState::Starting
981 | ManagedExecutionState::RestartStarting
982 | ManagedExecutionState::Running
983 | ManagedExecutionState::Pausing
984 | ManagedExecutionState::Killing => Ok(ExecutionState::Running),
985 ManagedExecutionState::Snapshotting => match record
986 .managed_execution
987 .as_ref()
988 .and_then(|metadata| metadata.pending_operation.as_ref())
989 {
990 Some(ManagedExecutionOperation::Snapshot {
991 source_state: ManagedExecutionState::Running,
992 ..
993 }) => Ok(ExecutionState::Running),
994 Some(ManagedExecutionOperation::Snapshot {
995 source_state: ManagedExecutionState::Paused,
996 ..
997 }) => Ok(ExecutionState::Paused),
998 _ => Err(ExecutionManagerError::Internal(format!(
999 "execution {} has invalid snapshot metadata",
1000 record.id
1001 ))),
1002 },
1003 ManagedExecutionState::RestartStopping => match record
1004 .managed_execution
1005 .as_ref()
1006 .and_then(|metadata| metadata.pending_operation.as_ref())
1007 {
1008 Some(ManagedExecutionOperation::Restart {
1009 source_state: ManagedExecutionState::Paused,
1010 ..
1011 }) => Ok(ExecutionState::Paused),
1012 Some(ManagedExecutionOperation::Restart {
1013 source_state: ManagedExecutionState::Running,
1014 ..
1015 }) => Ok(ExecutionState::Running),
1016 _ => Err(ExecutionManagerError::Internal(format!(
1017 "execution {} has invalid restart teardown metadata",
1018 record.id
1019 ))),
1020 },
1021 state => Err(ExecutionManagerError::Internal(format!(
1022 "execution {} has no active runtime in managed state {state}",
1023 record.id
1024 ))),
1025 }
1026}
1027
1028fn managed_state(record: &BoxRecord) -> ExecutionManagerResult<ManagedExecutionState> {
1029 record
1030 .managed_state()
1031 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?
1032 .ok_or_else(|| {
1033 ExecutionManagerError::Internal(format!("execution {} is not managed", record.id))
1034 })
1035}
1036
1037fn execution_id(record: &BoxRecord) -> ExecutionManagerResult<ExecutionId> {
1038 ExecutionId::new(record.id.clone())
1039 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
1040}
1041
1042fn runtime_error(
1043 action: &str,
1044 record: &BoxRecord,
1045 error: impl std::fmt::Display,
1046) -> ExecutionManagerError {
1047 ExecutionManagerError::Internal(format!(
1048 "failed to {action} execution {}: {error}",
1049 record.id
1050 ))
1051}
1052
1053fn unsupported(record: &BoxRecord, operation: &str, backend: &str) -> ExecutionManagerError {
1054 ExecutionManagerError::Unavailable(format!(
1055 "{operation} is not supported by {backend} for execution {}",
1056 record.id
1057 ))
1058}
1059
1060#[cfg(unix)]
1061async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1062 let Some(path) = path else {
1063 return false;
1064 };
1065 let attempt = async {
1066 let client = crate::ExecClient::connect(path).await.ok()?;
1067 client.heartbeat().await.ok().filter(|ready| *ready)
1068 };
1069 tokio::time::timeout(Duration::from_millis(500), attempt)
1070 .await
1071 .ok()
1072 .flatten()
1073 .is_some()
1074}
1075
1076#[cfg(not(unix))]
1077async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1078 path.is_some()
1079}
1080
1081#[cfg(test)]
1082#[path = "vm_backend_tests.rs"]
1083mod tests;