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(&record.id, anonymous_volumes)
477 .await;
478 }
479 Ok(LocalExecutionTermination {
480 outcome: KillOutcome::Killed,
481 exit_code,
482 })
483 }
484
485 async fn anonymous_volumes_for_record(&self, record: &BoxRecord) -> Vec<String> {
486 if !record.anonymous_volumes.is_empty() {
487 return record.anonymous_volumes.clone();
488 }
489 let home_dir = self.home_dir.clone();
490 let execution_id = record.id.clone();
491 let short_id = record.id.chars().take(8).collect::<String>();
492 let result = tokio::task::spawn_blocking(move || -> a3s_box_core::Result<Vec<String>> {
493 let store =
494 crate::VolumeStore::new(home_dir.join("volumes.json"), home_dir.join("volumes"));
495 let prefix = format!("anon_{short_id}_");
496 let mut names = store
497 .load()?
498 .into_values()
499 .filter(|volume| {
500 volume
501 .labels
502 .get("anonymous")
503 .is_some_and(|value| value == "true")
504 && (volume.in_use_by.iter().any(|id| id == &execution_id)
505 || volume.name.starts_with(&prefix))
506 })
507 .map(|volume| volume.name)
508 .collect::<Vec<_>>();
509 names.sort();
510 Ok(names)
511 })
512 .await;
513 match result {
514 Ok(Ok(names)) => names,
515 Ok(Err(error)) => {
516 tracing::warn!(
517 execution_id = %record.id,
518 %error,
519 "Failed to load anonymous volumes during managed cleanup"
520 );
521 Vec::new()
522 }
523 Err(error) => {
524 tracing::warn!(
525 execution_id = %record.id,
526 %error,
527 "Anonymous volume recovery task failed"
528 );
529 Vec::new()
530 }
531 }
532 }
533
534 async fn cleanup_anonymous_volumes(&self, owner: &str, names: Vec<String>) {
535 if names.is_empty() {
536 return;
537 }
538 let home_dir = self.home_dir.clone();
539 let owner = owner.to_string();
540 let task = tokio::task::spawn_blocking(move || {
541 let store = crate::VolumeStore::new(
542 home_dir.join("volumes.json"),
543 home_dir.join("volumes"),
544 );
545 for name in names {
546 if let Err(error) = store.remove_anonymous(&name, &owner) {
547 tracing::warn!(volume = %name, %error, "Failed to remove managed anonymous volume");
548 }
549 }
550 })
551 .await;
552 if let Err(error) = task {
553 tracing::warn!(%error, "Anonymous volume cleanup task failed");
554 }
555 }
556
557 async fn terminate_execution(
558 &self,
559 record: &BoxRecord,
560 ) -> ExecutionManagerResult<LocalExecutionTermination> {
561 let metadata = self.metadata(record)?;
562 let remove_anonymous_volumes = record.auto_remove;
563 let timeout_secs = record.stop_timeout;
564 if let Some(manager) = self.manager(&record.id) {
565 return self
566 .destroy_registered(
567 record,
568 manager,
569 remove_anonymous_volumes,
570 false,
571 timeout_secs,
572 )
573 .await;
574 }
575 if !metadata.paused_with_memory {
579 let manager = Arc::new(Mutex::new(self.new_manager(record)?));
580 return self
581 .destroy_registered(
582 record,
583 manager,
584 remove_anonymous_volumes,
585 false,
586 timeout_secs,
587 )
588 .await;
589 }
590 match metadata.plan.backend {
591 ExecutionBackend::A3sOci => {
592 self.destroy_detached_sandbox(record, remove_anonymous_volumes, false, timeout_secs)
593 .await
594 }
595 ExecutionBackend::Krun => {
596 let manager = self.recover_microvm(record).await?;
597 self.destroy_registered(
598 record,
599 manager,
600 remove_anonymous_volumes,
601 false,
602 timeout_secs,
603 )
604 .await
605 }
606 }
607 }
608}
609
610async fn destroy_after_observation(
611 manager: &mut VmManager,
612 preserve_rootfs: bool,
613) -> a3s_box_core::Result<()> {
614 if preserve_rootfs {
615 manager.destroy_preserving_rootfs().await
616 } else {
617 manager.destroy().await
618 }
619}
620
621#[async_trait]
622impl LocalExecutionBackend for VmLocalExecutionBackend {
623 fn route_for_create(
624 &self,
625 _record: &BoxRecord,
626 ) -> ExecutionManagerResult<crate::ManagedRuntimeRoute> {
627 Ok(crate::ManagedRuntimeRoute::BoxVm)
628 }
629
630 async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
631 super::record::validate_record_health(record)?;
632 self.metadata(record)?;
633 let box_dir = record.box_dir.clone();
634 let execution_id = record.id.clone();
635 tokio::task::spawn_blocking(move || {
636 crate::rootfs::stage_box_terminal_rootfs_metadata(&box_dir)
637 })
638 .await
639 .map_err(|error| {
640 ExecutionManagerError::Internal(format!(
641 "rootfs metadata staging task failed for {execution_id}: {error}"
642 ))
643 })?
644 .map_err(|error| {
645 ExecutionManagerError::Internal(format!(
646 "failed to stage rootfs metadata for {execution_id}: {error}"
647 ))
648 })?;
649 let mut manager = self.new_manager(record)?;
650 let requested_persistence = manager.config.persistent;
651 if should_reuse_preserved_rootfs(record)?
652 && crate::vm::persistent_rootfs_generation_exists(&record.box_dir)
653 .map_err(|error| runtime_error("inspect retained rootfs", record, error))?
654 {
655 manager.config.persistent = true;
656 }
657 let manager = Arc::new(Mutex::new(manager));
658 match self.managers.entry(record.id.clone()) {
659 Entry::Occupied(_) => {
660 return Err(ExecutionManagerError::Unavailable(format!(
661 "execution {} already has an in-process runtime owner",
662 record.id
663 )))
664 }
665 Entry::Vacant(entry) => {
666 entry.insert(Arc::clone(&manager));
667 }
668 }
669
670 let mut guard = manager.lock().await;
671 let resource_home = self.home_dir.clone();
672 let resource_record = record.clone();
673 let resources = match tokio::task::spawn_blocking(move || {
674 ExecutionResourceGuard::prepare(&resource_home, &resource_record)
675 })
676 .await
677 {
678 Ok(Ok(resources)) => resources,
679 Ok(Err(error)) => {
680 drop(guard);
681 self.remove_manager(&record.id, &manager);
682 return Err(error);
683 }
684 Err(error) => {
685 drop(guard);
686 self.remove_manager(&record.id, &manager);
687 return Err(ExecutionManagerError::Internal(format!(
688 "managed resource preparation task failed for {}: {error}",
689 record.id
690 )));
691 }
692 };
693 #[cfg(target_os = "linux")]
694 self.claim_transient_registry_auth_for_boot(&mut guard);
695 if let Err(error) = guard.boot().await {
696 guard.config.persistent = requested_persistence;
697 if guard.exit_code().is_some() {
704 tracing::debug!(
705 execution_id = %record.id,
706 %error,
707 "Runtime completed while startup was establishing readiness"
708 );
709 resources.disarm();
710 return Err(ExecutionManagerError::Unavailable(format!(
711 "execution {} completed during startup",
712 record.id
713 )));
714 }
715 drop(guard);
716 self.remove_manager(&record.id, &manager);
717 let rollback = tokio::task::spawn_blocking(move || resources.rollback()).await;
718 if let Err(rollback_error) = rollback {
719 tracing::warn!(
720 execution_id = %record.id,
721 %rollback_error,
722 "Managed resource rollback task failed"
723 );
724 }
725 return Err(runtime_error("start", record, error));
726 }
727 guard.config.persistent = requested_persistence;
728 resources.disarm();
729 let exited_during_start = guard
730 .try_wait_exit()
731 .await
732 .map_err(|error| runtime_error("collect startup exit status", record, error))?
733 .is_some()
734 || guard.has_exited().await;
735 if exited_during_start {
736 return Err(ExecutionManagerError::Unavailable(format!(
737 "execution {} completed during startup",
738 record.id
739 )));
740 }
741 self.handle_from_manager(record, &guard).await
742 }
743
744 async fn inspect(
745 &self,
746 record: &BoxRecord,
747 ) -> ExecutionManagerResult<LocalExecutionObservation> {
748 let metadata = self.metadata(record)?;
749 if metadata.plan.backend.is_sandbox() {
750 return self.inspect_sandbox(record).await;
751 }
752 if let Some(manager) = self.manager(&record.id) {
753 return self.inspect_registered(record, manager).await;
754 }
755 let manager = self.recover_microvm(record).await?;
756 self.inspect_registered(record, manager).await
757 }
758
759 async fn pause(
760 &self,
761 record: &BoxRecord,
762 keep_memory: bool,
763 ) -> ExecutionManagerResult<LocalExecutionHandle> {
764 let metadata = self.metadata(record)?;
765 if metadata.plan.backend.is_sandbox() {
766 if !keep_memory {
767 return Err(unsupported(
768 record,
769 "pause without memory retention",
770 "the Sandbox backend",
771 ));
772 }
773 return self.pause_sandbox(record).await;
774 }
775 if !keep_memory {
776 return Err(unsupported(
777 record,
778 "pause without memory retention",
779 "the local MicroVM backend",
780 ));
781 }
782 #[cfg(windows)]
783 {
784 Err(unsupported(
785 record,
786 "pause",
787 "the local MicroVM backend on Windows",
788 ))
789 }
790 #[cfg(not(windows))]
791 {
792 let shared = self.require_microvm(record).await?;
793 let manager = shared.lock().await;
794 require_recorded_pid(record, &manager).await?;
795 manager
796 .pause()
797 .await
798 .map_err(|error| runtime_error("pause", record, error))?;
799 self.handle_from_manager(record, &manager).await
800 }
801 }
802
803 async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
804 let metadata = self.metadata(record)?;
805 if metadata.plan.backend.is_sandbox() {
806 return self.resume_sandbox(record).await;
807 }
808 #[cfg(windows)]
809 {
810 Err(unsupported(
811 record,
812 "resume",
813 "the local MicroVM backend on Windows",
814 ))
815 }
816 #[cfg(not(windows))]
817 {
818 let shared = self.require_microvm(record).await?;
819 let manager = shared.lock().await;
820 require_recorded_pid(record, &manager).await?;
821 manager
822 .resume()
823 .await
824 .map_err(|error| runtime_error("resume", record, error))?;
825 self.handle_from_manager(record, &manager).await
826 }
827 }
828
829 async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
830 self.new_manager(record)?
831 .prepare_preserved_rootfs()
832 .map(|_| ())
833 .map_err(|error| runtime_error("prepare quiescent rootfs", record, error))
834 }
835
836 async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
837 self.new_manager(record)?
838 .cleanup_preserved_rootfs()
839 .map_err(|error| runtime_error("clean up quiescent rootfs", record, error))
840 }
841
842 async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
843 Ok(self.terminate_execution(record).await?.outcome)
844 }
845
846 async fn kill_with_status(
847 &self,
848 record: &BoxRecord,
849 ) -> ExecutionManagerResult<LocalExecutionTermination> {
850 self.terminate_execution(record).await
851 }
852
853 async fn stop_for_restart(
854 &self,
855 record: &BoxRecord,
856 timeout_secs: Option<u64>,
857 ) -> ExecutionManagerResult<KillOutcome> {
858 let metadata = self.metadata(record)?;
859 #[cfg(target_os = "linux")]
860 if metadata.plan.backend.is_sandbox() {
861 super::snapshot::persist_sandbox_snapshot_mappings(record)?;
862 }
863 let timeout_secs = timeout_secs.or(record.stop_timeout);
864 if let Some(manager) = self.manager(&record.id) {
865 return Ok(self
866 .destroy_registered(record, manager, false, true, timeout_secs)
867 .await?
868 .outcome);
869 }
870 match metadata.plan.backend {
871 ExecutionBackend::A3sOci => Ok(self
872 .destroy_detached_sandbox(record, false, true, timeout_secs)
873 .await?
874 .outcome),
875 ExecutionBackend::Krun => {
876 let manager = self.recover_microvm(record).await?;
877 Ok(self
878 .destroy_registered(record, manager, false, true, timeout_secs)
879 .await?
880 .outcome)
881 }
882 }
883 }
884}
885
886pub(super) fn should_force_rootfs_preservation(record: &BoxRecord) -> ExecutionManagerResult<bool> {
887 let state = super::support::managed_state(record)?;
888 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
889 ExecutionManagerError::Internal(format!(
890 "execution {} has no managed lifecycle metadata",
891 record.id
892 ))
893 })?;
894 Ok(match state {
895 ManagedExecutionState::Starting => true,
901 ManagedExecutionState::Pausing => matches!(
902 metadata.pending_operation.as_ref(),
903 Some(ManagedExecutionOperation::Pause {
904 keep_memory: false,
905 ..
906 })
907 ),
908 ManagedExecutionState::Resuming => !metadata.paused_with_memory,
909 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => true,
910 _ => false,
911 })
912}
913
914fn should_reuse_preserved_rootfs(record: &BoxRecord) -> ExecutionManagerResult<bool> {
915 Ok(matches!(
916 super::support::managed_state(record)?,
917 ManagedExecutionState::Resuming | ManagedExecutionState::RestartStarting
918 ) && should_force_rootfs_preservation(record)?)
919}
920
921fn graceful_stop_options(
922 record: &BoxRecord,
923 timeout_secs: Option<u64>,
924) -> ExecutionManagerResult<Option<(i32, u64)>> {
925 if timeout_secs.is_none() && record.stop_signal.is_none() {
926 return Ok(None);
927 }
928 let timeout_ms = timeout_secs
929 .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT_MS / 1_000)
930 .checked_mul(1_000)
931 .ok_or_else(|| {
932 ExecutionManagerError::InvalidRequest(format!(
933 "stop timeout is too large for execution {}",
934 record.id
935 ))
936 })?;
937 let signal = record
938 .stop_signal
939 .as_deref()
940 .map(a3s_box_core::vmm::parse_signal_name)
941 .unwrap_or(libc::SIGTERM);
942 Ok(Some((signal, timeout_ms)))
943}
944
945#[cfg(not(windows))]
946async fn require_recorded_pid(
947 record: &BoxRecord,
948 manager: &VmManager,
949) -> ExecutionManagerResult<()> {
950 let execution_id = execution_id(record)?;
951 let pid = manager
952 .pid()
953 .await
954 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
955 if record.pid != Some(pid)
956 || !crate::process::is_process_alive_with_identity(pid, record.pid_start_time)
957 {
958 return Err(ExecutionManagerError::NotFound(execution_id));
959 }
960 Ok(())
961}
962
963fn visible_active_state(record: &BoxRecord) -> ExecutionManagerResult<ExecutionState> {
964 match managed_state(record)? {
965 ManagedExecutionState::Paused => Ok(ExecutionState::Paused),
966 ManagedExecutionState::Resuming => {
967 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
968 ExecutionManagerError::Internal(format!(
969 "execution {} has no managed lifecycle metadata",
970 record.id
971 ))
972 })?;
973 if metadata.paused_with_memory {
974 Ok(ExecutionState::Paused)
975 } else {
976 Ok(ExecutionState::Running)
980 }
981 }
982 ManagedExecutionState::Starting
983 | ManagedExecutionState::RestartStarting
984 | ManagedExecutionState::Running
985 | ManagedExecutionState::Pausing
986 | ManagedExecutionState::Killing => Ok(ExecutionState::Running),
987 ManagedExecutionState::Snapshotting => match record
988 .managed_execution
989 .as_ref()
990 .and_then(|metadata| metadata.pending_operation.as_ref())
991 {
992 Some(ManagedExecutionOperation::Snapshot {
993 source_state: ManagedExecutionState::Running,
994 ..
995 }) => Ok(ExecutionState::Running),
996 Some(ManagedExecutionOperation::Snapshot {
997 source_state: ManagedExecutionState::Paused,
998 ..
999 }) => Ok(ExecutionState::Paused),
1000 _ => Err(ExecutionManagerError::Internal(format!(
1001 "execution {} has invalid snapshot metadata",
1002 record.id
1003 ))),
1004 },
1005 ManagedExecutionState::RestartStopping => match record
1006 .managed_execution
1007 .as_ref()
1008 .and_then(|metadata| metadata.pending_operation.as_ref())
1009 {
1010 Some(ManagedExecutionOperation::Restart {
1011 source_state: ManagedExecutionState::Paused,
1012 ..
1013 }) => Ok(ExecutionState::Paused),
1014 Some(ManagedExecutionOperation::Restart {
1015 source_state: ManagedExecutionState::Running,
1016 ..
1017 }) => Ok(ExecutionState::Running),
1018 _ => Err(ExecutionManagerError::Internal(format!(
1019 "execution {} has invalid restart teardown metadata",
1020 record.id
1021 ))),
1022 },
1023 state => Err(ExecutionManagerError::Internal(format!(
1024 "execution {} has no active runtime in managed state {state}",
1025 record.id
1026 ))),
1027 }
1028}
1029
1030fn managed_state(record: &BoxRecord) -> ExecutionManagerResult<ManagedExecutionState> {
1031 record
1032 .managed_state()
1033 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?
1034 .ok_or_else(|| {
1035 ExecutionManagerError::Internal(format!("execution {} is not managed", record.id))
1036 })
1037}
1038
1039fn execution_id(record: &BoxRecord) -> ExecutionManagerResult<ExecutionId> {
1040 ExecutionId::new(record.id.clone())
1041 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
1042}
1043
1044fn runtime_error(
1045 action: &str,
1046 record: &BoxRecord,
1047 error: impl std::fmt::Display,
1048) -> ExecutionManagerError {
1049 ExecutionManagerError::Internal(format!(
1050 "failed to {action} execution {}: {error}",
1051 record.id
1052 ))
1053}
1054
1055fn unsupported(record: &BoxRecord, operation: &str, backend: &str) -> ExecutionManagerError {
1056 ExecutionManagerError::Unavailable(format!(
1057 "{operation} is not supported by {backend} for execution {}",
1058 record.id
1059 ))
1060}
1061
1062#[cfg(unix)]
1063async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1064 let Some(path) = path else {
1065 return false;
1066 };
1067 let attempt = async {
1068 let client = crate::ExecClient::connect(path).await.ok()?;
1069 client.heartbeat().await.ok().filter(|ready| *ready)
1070 };
1071 tokio::time::timeout(Duration::from_millis(500), attempt)
1072 .await
1073 .ok()
1074 .flatten()
1075 .is_some()
1076}
1077
1078#[cfg(not(unix))]
1079async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
1080 path.is_some()
1081}
1082
1083#[cfg(test)]
1084#[path = "vm_backend_tests.rs"]
1085mod tests;