Skip to main content

a3s_box_runtime/local_execution/
vm_backend.rs

1//! Production local execution backend backed by [`crate::VmManager`].
2
3#[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/// Runtime adapter that owns live [`VmManager`] handles and reconstructs them
37/// from durable runtime evidence after a control-plane restart.
38#[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        // Network connect/disconnect is intentionally mutable while an
147        // execution is inactive. The managed creation request remains the
148        // immutable idempotency identity, so apply the current record-level
149        // network selection when constructing each runtime generation.
150        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                // The runtime state and health probes can still report a live
309                // Sandbox after its host process has exited. Treat the missing
310                // handle as the same terminal signal as a failed health probe
311                // and wait for the exact status before projecting provider
312                // loss. This closes the restart/replay race without creating a
313                // second lifecycle path.
314                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        // A workload can exit between the first non-blocking wait and the
336        // following health probe. A3S OCI publishes the exact status shortly
337        // after the process becomes non-running, so retain its terminal record
338        // and poll within a strict bound before any teardown.
339        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        // A filesystem-only pause deliberately has no live provider evidence,
574        // but a later terminal kill must still apply the configured rootfs and
575        // anonymous-volume cleanup policy to the retained generation.
576        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            // Sandbox boot cleanup synchronously asks the authoritative OCI
696            // handler for its exit status before tearing down transient host
697            // artifacts. A very short task can therefore complete while boot
698            // is still establishing readiness. Keep the manager whenever that
699            // cleanup captured an exact status so the normal inspect path can
700            // project it into the durable managed record.
701            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        // A foreground one-shot can finish before readiness is published. Its
894        // exact status is already terminal, but the caller still has to drain
895        // stdout/stderr and archive an auto-removed result. Tear down mounts
896        // and runtime processes while retaining the box directory until that
897        // caller completes its normal terminal cleanup.
898        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                // A cold-paused execution has no provider process. Any live
975                // process observed while its resume is pending is therefore
976                // the replacement generation started before record commit.
977                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;