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};
22use super::{LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation};
23use crate::{
24    BoxRecord, ManagedExecutionMetadata, ManagedExecutionOperation, ManagedExecutionState,
25    VmManager,
26};
27
28type SharedVm = Arc<Mutex<VmManager>>;
29
30/// Runtime adapter that owns live [`VmManager`] handles and reconstructs them
31/// from durable runtime evidence after a control-plane restart.
32#[derive(Clone)]
33pub struct VmLocalExecutionBackend {
34    home_dir: PathBuf,
35    managers: Arc<DashMap<String, SharedVm>>,
36    pull_progress_fn: Option<crate::PullProgressFn>,
37}
38
39impl VmLocalExecutionBackend {
40    pub fn new(home_dir: impl Into<PathBuf>) -> Self {
41        Self {
42            home_dir: home_dir.into(),
43            managers: Arc::new(DashMap::new()),
44            pull_progress_fn: None,
45        }
46    }
47
48    pub fn with_pull_progress_fn(mut self, pull_progress_fn: crate::PullProgressFn) -> Self {
49        self.pull_progress_fn = Some(pull_progress_fn);
50        self
51    }
52
53    pub fn home_dir(&self) -> &Path {
54        &self.home_dir
55    }
56
57    fn metadata<'a>(
58        &self,
59        record: &'a BoxRecord,
60    ) -> ExecutionManagerResult<&'a ManagedExecutionMetadata> {
61        uuid::Uuid::parse_str(&record.id).map_err(|error| {
62            ExecutionManagerError::Internal(format!(
63                "managed execution has an invalid internal ID {}: {error}",
64                record.id
65            ))
66        })?;
67        let expected_box_dir = self.home_dir.join("boxes").join(&record.id);
68        if record.box_dir != expected_box_dir {
69            return Err(ExecutionManagerError::Internal(format!(
70                "managed execution {} has an unexpected host directory {}",
71                record.id,
72                record.box_dir.display()
73            )));
74        }
75        let metadata = record.managed_execution.as_ref().ok_or_else(|| {
76            ExecutionManagerError::Internal(format!(
77                "execution {} lost managed lifecycle metadata",
78                record.id
79            ))
80        })?;
81        metadata
82            .validate()
83            .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
84        if record.isolation != metadata.request.config.isolation {
85            return Err(ExecutionManagerError::Internal(format!(
86                "managed execution {} has inconsistent isolation metadata",
87                record.id
88            )));
89        }
90        Ok(metadata)
91    }
92
93    fn new_manager(&self, record: &BoxRecord) -> ExecutionManagerResult<VmManager> {
94        let metadata = self.metadata(record)?;
95        let mut config = metadata.request.config.clone();
96        if let Some(shm_size) = metadata.request.policy.shm_size {
97            let has_shared_memory_mount = config
98                .tmpfs
99                .iter()
100                .any(|entry| entry.split(':').next() == Some("/dev/shm"));
101            if !has_shared_memory_mount {
102                config.tmpfs.push(format!("/dev/shm:size={shm_size}"));
103            }
104        }
105        let mut manager = VmManager::with_box_id(config, EventEmitter::new(256), record.id.clone());
106        manager.home_dir = self.home_dir.clone();
107        manager.set_healthcheck_disabled(metadata.request.policy.healthcheck_disabled);
108        if let Some(pull_progress_fn) = self.pull_progress_fn.clone() {
109            manager.set_pull_progress_fn(pull_progress_fn);
110        }
111        manager.anonymous_volumes = record.anonymous_volumes.clone();
112        manager.set_log_config(record.log_config.clone());
113        manager.resolved_execution_plan = Some(metadata.plan.clone());
114        Ok(manager)
115    }
116
117    fn manager(&self, execution_id: &str) -> Option<SharedVm> {
118        self.managers
119            .get(execution_id)
120            .map(|entry| Arc::clone(entry.value()))
121    }
122
123    fn remove_manager(&self, execution_id: &str, expected: &SharedVm) {
124        if let Entry::Occupied(entry) = self.managers.entry(execution_id.to_string()) {
125            if Arc::ptr_eq(entry.get(), expected) {
126                entry.remove();
127            }
128        }
129    }
130
131    async fn handle_from_manager(
132        &self,
133        record: &BoxRecord,
134        manager: &VmManager,
135    ) -> ExecutionManagerResult<LocalExecutionHandle> {
136        let execution_id = execution_id(record)?;
137        let pid = manager.pid().await.ok_or_else(|| {
138            ExecutionManagerError::Internal(format!(
139                "runtime returned no host PID for {execution_id}"
140            ))
141        })?;
142        let pid_start_time = crate::process::pid_start_time(pid);
143        #[cfg(target_os = "linux")]
144        if pid_start_time.is_none() {
145            return Err(ExecutionManagerError::NotFound(execution_id));
146        }
147        if !crate::process::is_process_alive_with_identity(pid, pid_start_time) {
148            return Err(ExecutionManagerError::NotFound(execution_id));
149        }
150        let exec_socket_path = manager
151            .exec_socket_path()
152            .map(Path::to_path_buf)
153            .ok_or_else(|| {
154                ExecutionManagerError::Internal(format!(
155                    "runtime returned no exec socket for {}",
156                    record.id
157                ))
158            })?;
159        let anonymous_volumes = if manager.anonymous_volumes().is_empty() {
160            self.anonymous_volumes_for_record(record).await
161        } else {
162            manager.anonymous_volumes().to_vec()
163        };
164        Ok(LocalExecutionHandle {
165            started_at: record.started_at.unwrap_or_else(chrono::Utc::now),
166            pid: Some(pid),
167            pid_start_time,
168            exec_socket_path,
169            console_log: record.box_dir.join("logs/console.log"),
170            anonymous_volumes,
171        })
172    }
173
174    async fn inspect_registered(
175        &self,
176        record: &BoxRecord,
177        shared: SharedVm,
178    ) -> ExecutionManagerResult<LocalExecutionObservation> {
179        let mut manager = shared.lock().await;
180        let preserve_rootfs = should_force_rootfs_preservation(record)?;
181        let exit_code = manager
182            .try_wait_exit()
183            .await
184            .map_err(|error| runtime_error("inspect", record, error))?;
185        let mut state = manager.state().await;
186        let terminal = exit_code.is_some() || state == crate::BoxState::Stopped;
187        if terminal {
188            let cleanup = destroy_after_observation(&mut manager, preserve_rootfs).await;
189            let exit_code = manager.exit_code().or(exit_code);
190            drop(manager);
191            self.remove_manager(&record.id, &shared);
192            cleanup.map_err(|error| runtime_error("clean up", record, error))?;
193            return Ok(LocalExecutionObservation {
194                state: ExecutionState::Stopped,
195                handle: None,
196                exit_code,
197            });
198        }
199
200        if state == crate::BoxState::Created {
201            if manager.has_exited().await {
202                let cleanup = destroy_after_observation(&mut manager, preserve_rootfs).await;
203                let exit_code = manager.exit_code();
204                drop(manager);
205                self.remove_manager(&record.id, &shared);
206                cleanup.map_err(|error| runtime_error("clean up", record, error))?;
207                return Ok(LocalExecutionObservation {
208                    state: ExecutionState::Stopped,
209                    handle: None,
210                    exit_code,
211                });
212            }
213            if !self.promote_if_ready(record, &mut manager).await {
214                return Ok(LocalExecutionObservation {
215                    state: ExecutionState::Creating,
216                    handle: None,
217                    exit_code: None,
218                });
219            }
220            state = manager.state().await;
221        }
222
223        if !manager
224            .health_check()
225            .await
226            .map_err(|error| runtime_error("inspect", record, error))?
227        {
228            let cleanup = destroy_after_observation(&mut manager, preserve_rootfs).await;
229            let exit_code = manager.exit_code();
230            drop(manager);
231            self.remove_manager(&record.id, &shared);
232            cleanup.map_err(|error| runtime_error("clean up", record, error))?;
233            return Ok(LocalExecutionObservation {
234                state: ExecutionState::Stopped,
235                handle: None,
236                exit_code,
237            });
238        }
239
240        if state != crate::BoxState::Ready
241            && state != crate::BoxState::Busy
242            && state != crate::BoxState::Compacting
243        {
244            return Err(ExecutionManagerError::Internal(format!(
245                "runtime manager for {} is in unexpected state {state:?}",
246                record.id
247            )));
248        }
249        if matches!(
250            managed_state(record)?,
251            ManagedExecutionState::Starting | ManagedExecutionState::RestartStarting
252        ) && !exec_endpoint_ready(manager.exec_socket_path()).await
253        {
254            return Ok(LocalExecutionObservation {
255                state: ExecutionState::Creating,
256                handle: None,
257                exit_code: None,
258            });
259        }
260        let visible_state = visible_active_state(record)?;
261        let handle = self.handle_from_manager(record, &manager).await?;
262        Ok(LocalExecutionObservation {
263            state: visible_state,
264            handle: Some(handle),
265            exit_code: None,
266        })
267    }
268
269    async fn promote_if_ready(&self, record: &BoxRecord, manager: &mut VmManager) -> bool {
270        let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
271        let exec_socket = socket_dir.join("exec.sock");
272        if !exec_endpoint_ready(Some(&exec_socket)).await {
273            return false;
274        }
275        manager.exec_socket_path = Some(exec_socket);
276        manager.pty_socket_path = Some(socket_dir.join("pty.sock"));
277        manager.port_forward_socket_path = Some(socket_dir.join("portfwd.sock"));
278        *manager.state.write().await = crate::BoxState::Ready;
279        true
280    }
281
282    async fn recover_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
283        self.metadata(record)?;
284        let execution_id = execution_id(record)?;
285        let execution_id_label = record.id.clone();
286        let recorded = record.pid.map(|pid| (pid, record.pid_start_time));
287        let located = tokio::task::spawn_blocking(move || {
288            locate_microvm_process(&execution_id_label, recorded)
289        })
290        .await
291        .map_err(|error| {
292            ExecutionManagerError::Internal(format!(
293                "MicroVM process discovery task failed for {}: {error}",
294                record.id
295            ))
296        })?
297        .map_err(ExecutionManagerError::Internal)?
298        .ok_or(ExecutionManagerError::NotFound(execution_id))?;
299        self.attach_microvm(record, located).await
300    }
301
302    async fn attach_microvm(
303        &self,
304        record: &BoxRecord,
305        located: LocatedProcess,
306    ) -> ExecutionManagerResult<SharedVm> {
307        let mut manager = self.new_manager(record)?;
308        let socket_dir = crate::vm::runtime_socket_dir(&self.home_dir, &record.id);
309        manager
310            .attach_running_process(
311                located.pid,
312                socket_dir.join("exec.sock"),
313                Some(socket_dir.join("pty.sock")),
314            )
315            .await
316            .map_err(|error| runtime_error("recover", record, error))?;
317        if located.start_time.is_some()
318            && crate::process::pid_start_time(located.pid) != located.start_time
319        {
320            return Err(ExecutionManagerError::NotFound(execution_id(record)?));
321        }
322        let recovered = Arc::new(Mutex::new(manager));
323        match self.managers.entry(record.id.clone()) {
324            Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
325            Entry::Vacant(entry) => {
326                entry.insert(Arc::clone(&recovered));
327                Ok(recovered)
328            }
329        }
330    }
331
332    async fn require_microvm(&self, record: &BoxRecord) -> ExecutionManagerResult<SharedVm> {
333        match self.manager(&record.id) {
334            Some(manager) => Ok(manager),
335            None => self.recover_microvm(record).await,
336        }
337    }
338
339    async fn destroy_registered(
340        &self,
341        record: &BoxRecord,
342        shared: SharedVm,
343        remove_anonymous_volumes: bool,
344        force_preserve_rootfs: bool,
345        timeout_secs: Option<u64>,
346    ) -> ExecutionManagerResult<KillOutcome> {
347        let mut manager = shared.lock().await;
348        let mut anonymous_volumes = if manager.anonymous_volumes().is_empty() {
349            record.anonymous_volumes.clone()
350        } else {
351            manager.anonymous_volumes().to_vec()
352        };
353        let result = match (
354            graceful_stop_options(record, timeout_secs)?,
355            force_preserve_rootfs,
356        ) {
357            (Some((signal, timeout_ms)), true) => {
358                manager
359                    .destroy_preserving_rootfs_with_options(signal, timeout_ms)
360                    .await
361            }
362            (Some((signal, timeout_ms)), false) => {
363                manager.destroy_with_options(signal, timeout_ms).await
364            }
365            (None, true) => manager.destroy_preserving_rootfs().await,
366            (None, false) => manager.destroy().await,
367        };
368        drop(manager);
369        self.remove_manager(&record.id, &shared);
370        result.map_err(|error| runtime_error("kill", record, error))?;
371        if remove_anonymous_volumes {
372            if anonymous_volumes.is_empty() {
373                anonymous_volumes = self.anonymous_volumes_for_record(record).await;
374            }
375            self.cleanup_anonymous_volumes(anonymous_volumes).await;
376        }
377        Ok(KillOutcome::Killed)
378    }
379
380    async fn anonymous_volumes_for_record(&self, record: &BoxRecord) -> Vec<String> {
381        if !record.anonymous_volumes.is_empty() {
382            return record.anonymous_volumes.clone();
383        }
384        let home_dir = self.home_dir.clone();
385        let execution_id = record.id.clone();
386        let short_id = record.id.chars().take(8).collect::<String>();
387        let result = tokio::task::spawn_blocking(move || -> a3s_box_core::Result<Vec<String>> {
388            let store =
389                crate::VolumeStore::new(home_dir.join("volumes.json"), home_dir.join("volumes"));
390            let prefix = format!("anon_{short_id}_");
391            let mut names = store
392                .load()?
393                .into_values()
394                .filter(|volume| {
395                    volume
396                        .labels
397                        .get("anonymous")
398                        .is_some_and(|value| value == "true")
399                        && (volume.in_use_by.iter().any(|id| id == &execution_id)
400                            || volume.name.starts_with(&prefix))
401                })
402                .map(|volume| volume.name)
403                .collect::<Vec<_>>();
404            names.sort();
405            Ok(names)
406        })
407        .await;
408        match result {
409            Ok(Ok(names)) => names,
410            Ok(Err(error)) => {
411                tracing::warn!(
412                    execution_id = %record.id,
413                    %error,
414                    "Failed to load anonymous volumes during managed cleanup"
415                );
416                Vec::new()
417            }
418            Err(error) => {
419                tracing::warn!(
420                    execution_id = %record.id,
421                    %error,
422                    "Anonymous volume recovery task failed"
423                );
424                Vec::new()
425            }
426        }
427    }
428
429    async fn cleanup_anonymous_volumes(&self, names: Vec<String>) {
430        if names.is_empty() {
431            return;
432        }
433        let home_dir = self.home_dir.clone();
434        let task = tokio::task::spawn_blocking(move || {
435            let store = crate::VolumeStore::new(
436                home_dir.join("volumes.json"),
437                home_dir.join("volumes"),
438            );
439            for name in names {
440                if let Err(error) = store.remove(&name, true) {
441                    tracing::warn!(volume = %name, %error, "Failed to remove managed anonymous volume");
442                }
443            }
444        })
445        .await;
446        if let Err(error) = task {
447            tracing::warn!(%error, "Anonymous volume cleanup task failed");
448        }
449    }
450}
451
452async fn destroy_after_observation(
453    manager: &mut VmManager,
454    preserve_rootfs: bool,
455) -> a3s_box_core::Result<()> {
456    if preserve_rootfs {
457        manager.destroy_preserving_rootfs().await
458    } else {
459        manager.destroy().await
460    }
461}
462
463#[async_trait]
464impl LocalExecutionBackend for VmLocalExecutionBackend {
465    async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
466        super::record::validate_record_health(record)?;
467        self.metadata(record)?;
468        let box_dir = record.box_dir.clone();
469        let execution_id = record.id.clone();
470        tokio::task::spawn_blocking(move || {
471            crate::rootfs::stage_box_terminal_rootfs_metadata(&box_dir)
472        })
473        .await
474        .map_err(|error| {
475            ExecutionManagerError::Internal(format!(
476                "rootfs metadata staging task failed for {execution_id}: {error}"
477            ))
478        })?
479        .map_err(|error| {
480            ExecutionManagerError::Internal(format!(
481                "failed to stage rootfs metadata for {execution_id}: {error}"
482            ))
483        })?;
484        let mut manager = self.new_manager(record)?;
485        let requested_persistence = manager.config.persistent;
486        if should_reuse_preserved_rootfs(record)?
487            && crate::vm::persistent_rootfs_generation_exists(&record.box_dir)
488                .map_err(|error| runtime_error("inspect retained rootfs", record, error))?
489        {
490            manager.config.persistent = true;
491        }
492        let manager = Arc::new(Mutex::new(manager));
493        match self.managers.entry(record.id.clone()) {
494            Entry::Occupied(_) => {
495                return Err(ExecutionManagerError::Unavailable(format!(
496                    "execution {} already has an in-process runtime owner",
497                    record.id
498                )))
499            }
500            Entry::Vacant(entry) => {
501                entry.insert(Arc::clone(&manager));
502            }
503        }
504
505        let mut guard = manager.lock().await;
506        let resource_home = self.home_dir.clone();
507        let resource_record = record.clone();
508        let resources = match tokio::task::spawn_blocking(move || {
509            ExecutionResourceGuard::prepare(&resource_home, &resource_record)
510        })
511        .await
512        {
513            Ok(Ok(resources)) => resources,
514            Ok(Err(error)) => {
515                drop(guard);
516                self.remove_manager(&record.id, &manager);
517                return Err(error);
518            }
519            Err(error) => {
520                drop(guard);
521                self.remove_manager(&record.id, &manager);
522                return Err(ExecutionManagerError::Internal(format!(
523                    "managed resource preparation task failed for {}: {error}",
524                    record.id
525                )));
526            }
527        };
528        if let Err(error) = guard.boot().await {
529            drop(guard);
530            self.remove_manager(&record.id, &manager);
531            let rollback = tokio::task::spawn_blocking(move || resources.rollback()).await;
532            if let Err(rollback_error) = rollback {
533                tracing::warn!(
534                    execution_id = %record.id,
535                    %rollback_error,
536                    "Managed resource rollback task failed"
537                );
538            }
539            return Err(runtime_error("start", record, error));
540        }
541        guard.config.persistent = requested_persistence;
542        resources.disarm();
543        self.handle_from_manager(record, &guard).await
544    }
545
546    async fn inspect(
547        &self,
548        record: &BoxRecord,
549    ) -> ExecutionManagerResult<LocalExecutionObservation> {
550        let metadata = self.metadata(record)?;
551        if metadata.plan.backend == ExecutionBackend::Crun {
552            return self.inspect_sandbox(record).await;
553        }
554        if let Some(manager) = self.manager(&record.id) {
555            return self.inspect_registered(record, manager).await;
556        }
557        let manager = self.recover_microvm(record).await?;
558        self.inspect_registered(record, manager).await
559    }
560
561    async fn pause(
562        &self,
563        record: &BoxRecord,
564        keep_memory: bool,
565    ) -> ExecutionManagerResult<LocalExecutionHandle> {
566        let metadata = self.metadata(record)?;
567        if metadata.plan.backend == ExecutionBackend::Crun {
568            if !keep_memory {
569                return Err(unsupported(
570                    record,
571                    "pause without memory retention",
572                    "the Sandbox backend",
573                ));
574            }
575            return self.pause_sandbox(record).await;
576        }
577        if !keep_memory {
578            return Err(unsupported(
579                record,
580                "pause without memory retention",
581                "the local MicroVM backend",
582            ));
583        }
584        let shared = self.require_microvm(record).await?;
585        let manager = shared.lock().await;
586        require_recorded_pid(record, &manager).await?;
587        manager
588            .pause()
589            .await
590            .map_err(|error| runtime_error("pause", record, error))?;
591        self.handle_from_manager(record, &manager).await
592    }
593
594    async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
595        let metadata = self.metadata(record)?;
596        if metadata.plan.backend == ExecutionBackend::Crun {
597            return self.resume_sandbox(record).await;
598        }
599        let shared = self.require_microvm(record).await?;
600        let manager = shared.lock().await;
601        require_recorded_pid(record, &manager).await?;
602        manager
603            .resume()
604            .await
605            .map_err(|error| runtime_error("resume", record, error))?;
606        self.handle_from_manager(record, &manager).await
607    }
608
609    async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
610        self.new_manager(record)?
611            .prepare_preserved_rootfs()
612            .map(|_| ())
613            .map_err(|error| runtime_error("prepare quiescent rootfs", record, error))
614    }
615
616    async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
617        self.new_manager(record)?
618            .cleanup_preserved_rootfs()
619            .map_err(|error| runtime_error("clean up quiescent rootfs", record, error))
620    }
621
622    async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
623        let metadata = self.metadata(record)?;
624        let remove_anonymous_volumes = record.auto_remove;
625        let timeout_secs = record.stop_timeout;
626        if let Some(manager) = self.manager(&record.id) {
627            return self
628                .destroy_registered(
629                    record,
630                    manager,
631                    remove_anonymous_volumes,
632                    false,
633                    timeout_secs,
634                )
635                .await;
636        }
637        // A filesystem-only pause deliberately has no live provider evidence,
638        // but a later terminal kill must still apply the configured rootfs and
639        // anonymous-volume cleanup policy to the retained generation.
640        if !metadata.paused_with_memory {
641            let manager = Arc::new(Mutex::new(self.new_manager(record)?));
642            return self
643                .destroy_registered(
644                    record,
645                    manager,
646                    remove_anonymous_volumes,
647                    false,
648                    timeout_secs,
649                )
650                .await;
651        }
652        match metadata.plan.backend {
653            ExecutionBackend::Crun => {
654                self.destroy_detached_sandbox(record, remove_anonymous_volumes, false, timeout_secs)
655                    .await
656            }
657            ExecutionBackend::Krun => {
658                let manager = self.recover_microvm(record).await?;
659                self.destroy_registered(
660                    record,
661                    manager,
662                    remove_anonymous_volumes,
663                    false,
664                    timeout_secs,
665                )
666                .await
667            }
668        }
669    }
670
671    async fn stop_for_restart(
672        &self,
673        record: &BoxRecord,
674        timeout_secs: Option<u64>,
675    ) -> ExecutionManagerResult<KillOutcome> {
676        let metadata = self.metadata(record)?;
677        #[cfg(target_os = "linux")]
678        if metadata.plan.backend == ExecutionBackend::Crun {
679            super::snapshot::persist_sandbox_snapshot_mappings(record)?;
680        }
681        let timeout_secs = timeout_secs.or(record.stop_timeout);
682        if let Some(manager) = self.manager(&record.id) {
683            return self
684                .destroy_registered(record, manager, false, true, timeout_secs)
685                .await;
686        }
687        match metadata.plan.backend {
688            ExecutionBackend::Crun => {
689                self.destroy_detached_sandbox(record, false, true, timeout_secs)
690                    .await
691            }
692            ExecutionBackend::Krun => {
693                let manager = self.recover_microvm(record).await?;
694                self.destroy_registered(record, manager, false, true, timeout_secs)
695                    .await
696            }
697        }
698    }
699}
700
701pub(super) fn should_force_rootfs_preservation(record: &BoxRecord) -> ExecutionManagerResult<bool> {
702    let state = super::support::managed_state(record)?;
703    let metadata = record.managed_execution.as_ref().ok_or_else(|| {
704        ExecutionManagerError::Internal(format!(
705            "execution {} has no managed lifecycle metadata",
706            record.id
707        ))
708    })?;
709    Ok(match state {
710        ManagedExecutionState::Pausing => matches!(
711            metadata.pending_operation.as_ref(),
712            Some(ManagedExecutionOperation::Pause { keep_memory: false })
713        ),
714        ManagedExecutionState::Resuming => !metadata.paused_with_memory,
715        ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => true,
716        _ => false,
717    })
718}
719
720fn should_reuse_preserved_rootfs(record: &BoxRecord) -> ExecutionManagerResult<bool> {
721    Ok(matches!(
722        super::support::managed_state(record)?,
723        ManagedExecutionState::Resuming | ManagedExecutionState::RestartStarting
724    ) && should_force_rootfs_preservation(record)?)
725}
726
727fn graceful_stop_options(
728    record: &BoxRecord,
729    timeout_secs: Option<u64>,
730) -> ExecutionManagerResult<Option<(i32, u64)>> {
731    if timeout_secs.is_none() && record.stop_signal.is_none() {
732        return Ok(None);
733    }
734    let timeout_ms = timeout_secs
735        .unwrap_or(DEFAULT_SHUTDOWN_TIMEOUT_MS / 1_000)
736        .checked_mul(1_000)
737        .ok_or_else(|| {
738            ExecutionManagerError::InvalidRequest(format!(
739                "stop timeout is too large for execution {}",
740                record.id
741            ))
742        })?;
743    let signal = record
744        .stop_signal
745        .as_deref()
746        .map(a3s_box_core::vmm::parse_signal_name)
747        .unwrap_or(libc::SIGTERM);
748    Ok(Some((signal, timeout_ms)))
749}
750
751async fn require_recorded_pid(
752    record: &BoxRecord,
753    manager: &VmManager,
754) -> ExecutionManagerResult<()> {
755    let execution_id = execution_id(record)?;
756    let pid = manager
757        .pid()
758        .await
759        .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
760    if record.pid != Some(pid)
761        || !crate::process::is_process_alive_with_identity(pid, record.pid_start_time)
762    {
763        return Err(ExecutionManagerError::NotFound(execution_id));
764    }
765    Ok(())
766}
767
768fn visible_active_state(record: &BoxRecord) -> ExecutionManagerResult<ExecutionState> {
769    match managed_state(record)? {
770        ManagedExecutionState::Paused => Ok(ExecutionState::Paused),
771        ManagedExecutionState::Resuming => {
772            let metadata = record.managed_execution.as_ref().ok_or_else(|| {
773                ExecutionManagerError::Internal(format!(
774                    "execution {} has no managed lifecycle metadata",
775                    record.id
776                ))
777            })?;
778            if metadata.paused_with_memory {
779                Ok(ExecutionState::Paused)
780            } else {
781                // A cold-paused execution has no provider process. Any live
782                // process observed while its resume is pending is therefore
783                // the replacement generation started before record commit.
784                Ok(ExecutionState::Running)
785            }
786        }
787        ManagedExecutionState::Starting
788        | ManagedExecutionState::RestartStarting
789        | ManagedExecutionState::Running
790        | ManagedExecutionState::Pausing
791        | ManagedExecutionState::Killing => Ok(ExecutionState::Running),
792        ManagedExecutionState::Snapshotting => match record
793            .managed_execution
794            .as_ref()
795            .and_then(|metadata| metadata.pending_operation.as_ref())
796        {
797            Some(ManagedExecutionOperation::Snapshot {
798                source_state: ManagedExecutionState::Running,
799                ..
800            }) => Ok(ExecutionState::Running),
801            Some(ManagedExecutionOperation::Snapshot {
802                source_state: ManagedExecutionState::Paused,
803                ..
804            }) => Ok(ExecutionState::Paused),
805            _ => Err(ExecutionManagerError::Internal(format!(
806                "execution {} has invalid snapshot metadata",
807                record.id
808            ))),
809        },
810        ManagedExecutionState::RestartStopping => match record
811            .managed_execution
812            .as_ref()
813            .and_then(|metadata| metadata.pending_operation.as_ref())
814        {
815            Some(ManagedExecutionOperation::Restart {
816                source_state: ManagedExecutionState::Paused,
817                ..
818            }) => Ok(ExecutionState::Paused),
819            Some(ManagedExecutionOperation::Restart {
820                source_state: ManagedExecutionState::Running,
821                ..
822            }) => Ok(ExecutionState::Running),
823            _ => Err(ExecutionManagerError::Internal(format!(
824                "execution {} has invalid restart teardown metadata",
825                record.id
826            ))),
827        },
828        state => Err(ExecutionManagerError::Internal(format!(
829            "execution {} has no active runtime in managed state {state}",
830            record.id
831        ))),
832    }
833}
834
835fn managed_state(record: &BoxRecord) -> ExecutionManagerResult<ManagedExecutionState> {
836    record
837        .managed_state()
838        .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?
839        .ok_or_else(|| {
840            ExecutionManagerError::Internal(format!("execution {} is not managed", record.id))
841        })
842}
843
844fn execution_id(record: &BoxRecord) -> ExecutionManagerResult<ExecutionId> {
845    ExecutionId::new(record.id.clone())
846        .map_err(|error| ExecutionManagerError::Internal(error.to_string()))
847}
848
849fn runtime_error(
850    action: &str,
851    record: &BoxRecord,
852    error: impl std::fmt::Display,
853) -> ExecutionManagerError {
854    ExecutionManagerError::Internal(format!(
855        "failed to {action} execution {}: {error}",
856        record.id
857    ))
858}
859
860fn unsupported(record: &BoxRecord, operation: &str, backend: &str) -> ExecutionManagerError {
861    match execution_id(record) {
862        Ok(execution_id) => ExecutionManagerError::Conflict {
863            execution_id,
864            message: format!("{operation} is not supported by {backend}"),
865        },
866        Err(error) => error,
867    }
868}
869
870#[cfg(unix)]
871async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
872    let Some(path) = path else {
873        return false;
874    };
875    let attempt = async {
876        let client = crate::ExecClient::connect(path).await.ok()?;
877        client.heartbeat().await.ok().filter(|ready| *ready)
878    };
879    tokio::time::timeout(Duration::from_millis(500), attempt)
880        .await
881        .ok()
882        .flatten()
883        .is_some()
884}
885
886#[cfg(not(unix))]
887async fn exec_endpoint_ready(path: Option<&Path>) -> bool {
888    path.is_some()
889}
890
891#[cfg(test)]
892#[path = "vm_backend_tests.rs"]
893mod tests;