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};
22use super::{LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation};
23use crate::{
24 BoxRecord, ManagedExecutionMetadata, ManagedExecutionOperation, ManagedExecutionState,
25 VmManager,
26};
27
28type SharedVm = Arc<Mutex<VmManager>>;
29
30#[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 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 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;