1mod boot;
4mod execution;
5mod layout;
6mod lifecycle;
7mod maintenance;
8mod network;
9mod oci_microvm;
10mod ready;
11pub mod reap;
12mod sandbox;
13mod spec;
14#[cfg(windows)]
15mod windows_stop;
16
17pub(crate) use layout::{
18 legacy_sandbox_runtime_root, persistent_rootfs_generation_exists, runtime_socket_dir,
19 sandbox_runtime_root,
20};
21pub use maintenance::archive_stopped_guest_native_rootfs;
22
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25
26pub type PullProgressFn = Arc<dyn Fn(usize, usize, &str, i64) + Send + Sync>;
28
29use a3s_box_core::config::BoxConfig;
30#[cfg(unix)]
31use a3s_box_core::config::TeeConfig;
32use a3s_box_core::error::{BoxError, Result};
33use a3s_box_core::event::{BoxEvent, EventEmitter};
34use a3s_box_core::execution::ResolvedExecutionPlan;
35use serde::{Deserialize, Serialize};
36use tokio::sync::RwLock;
37use tracing::Instrument;
38
39#[cfg(unix)]
40use libc;
41
42#[cfg(unix)]
43use crate::grpc::ExecClient;
44#[cfg(unix)]
45use crate::tee::TeeExtension;
46use crate::vmm::{VmController, VmHandler, VmmProvider, DEFAULT_SHUTDOWN_TIMEOUT_MS};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum BoxState {
51 Created,
53
54 Ready,
56
57 Busy,
59
60 Compacting,
62
63 Stopped,
65}
66
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68enum VmBootMode {
69 #[default]
70 Workload,
71 RootfsMaintenance,
72}
73
74pub(crate) struct BoxLayout {
76 pub(crate) rootfs_path: PathBuf,
82 pub(crate) resumed_rootfs: Option<crate::rootfs::ResumedRootfs>,
85 pub(crate) exec_socket_path: PathBuf,
87 pub(crate) pty_socket_path: PathBuf,
89 pub(crate) attest_socket_path: PathBuf,
91 pub(crate) port_forward_socket_path: PathBuf,
93 pub(crate) workspace_path: PathBuf,
95 pub(crate) console_output: Option<PathBuf>,
97 pub(crate) oci_config: Option<crate::oci::OciImageConfig>,
99 #[cfg(target_os = "macos")]
103 pub(crate) oci_manifest_digest: Option<String>,
104 pub(crate) prefer_image_rootfs_metadata: bool,
108 pub(crate) tee_instance_config: Option<crate::vmm::TeeInstanceConfig>,
110}
111
112#[cfg(target_os = "windows")]
113const WINDOWS_GUEST_EXIT_CODE: &str = ".a3s_exit_code";
114#[cfg(target_os = "windows")]
115const WINDOWS_GUEST_STDOUT: &str = "guest-init.stdout.log";
116#[cfg(target_os = "windows")]
117const WINDOWS_GUEST_STDERR: &str = "guest-init.stderr.log";
118#[cfg(target_os = "windows")]
119const WINDOWS_STOP_DELIVERY_TIMEOUT_MS: u64 = 1_000;
120#[cfg(target_os = "windows")]
121const WINDOWS_GUEST_FINALIZATION_TIMEOUT_MS: u64 = 30_000;
122#[cfg(target_os = "windows")]
123const WINDOWS_GUEST_RESULT_MARKER: &str = ".a3s_host_result_collected";
124#[cfg(target_os = "windows")]
125const WINDOWS_LIVE_LOGS_DRAINED_MARKER: &str = ".a3s_host_live_logs_drained";
126
127pub(crate) const TERMINAL_EXIT_POLL_INTERVAL: std::time::Duration =
128 std::time::Duration::from_millis(25);
129pub(crate) const TERMINAL_EXIT_POLL_TIMEOUT: std::time::Duration =
130 std::time::Duration::from_secs(5);
131
132#[cfg(target_os = "windows")]
135fn append_windows_guest_stream(
136 source: &Path,
137 destination: &Path,
138 runtime_filter: &a3s_box_core::log::RuntimeConsoleFilter,
139) -> std::io::Result<()> {
140 use std::io::{BufRead, Write};
141
142 let input = match a3s_box_core::windows_file::open_regular_file(source, None) {
143 Ok((input, _)) => input,
144 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
145 Err(error) => return Err(error),
146 };
147 let mut reader = std::io::BufReader::new(input);
148 let mut output = std::fs::OpenOptions::new()
149 .create(true)
150 .append(true)
151 .open(destination)?;
152 let mut line = Vec::new();
153
154 loop {
155 line.clear();
156 if reader.read_until(b'\n', &mut line)? == 0 {
157 break;
158 }
159 let keep = !line.ends_with(b"\n")
160 || std::str::from_utf8(&line).map_or(true, |line| runtime_filter.keep_line(line));
161 if keep {
162 output.write_all(&line)?;
163 }
164 }
165
166 output.flush()
167}
168
169#[cfg(target_os = "windows")]
170fn windows_marker_matches(path: &Path, expected: &[u8]) -> bool {
171 use std::io::Read;
172
173 let Ok((file, _)) = a3s_box_core::windows_file::open_regular_file(path, None) else {
174 return false;
175 };
176 let mut contents = Vec::with_capacity(expected.len().saturating_add(1));
177 if file
178 .take(expected.len().saturating_add(1) as u64)
179 .read_to_end(&mut contents)
180 .is_err()
181 {
182 return false;
183 }
184 contents == expected
185}
186
187#[cfg(target_os = "windows")]
194fn windows_guest_persisted_exit_code(box_dir: &Path) -> Option<i32> {
195 use std::io::Read;
196
197 let exit_path = box_dir.join("rootfs").join(WINDOWS_GUEST_EXIT_CODE);
198 let (file, _) = a3s_box_core::windows_file::open_regular_file(&exit_path, None).ok()?;
199 let mut contents = String::new();
200 file.take(64).read_to_string(&mut contents).ok()?;
201 contents.trim().parse::<i32>().ok()
202}
203
204#[cfg(target_os = "windows")]
210pub fn collect_windows_guest_result(
211 box_dir: &Path,
212 log_config: &a3s_box_core::log::LogConfig,
213 shim_exit_code: i32,
214) -> Result<i32> {
215 let rootfs = box_dir.join("rootfs");
216 let logs = box_dir.join("logs");
217 let marker = rootfs.join(WINDOWS_GUEST_RESULT_MARKER);
218 let live_logs_drained = rootfs.join(WINDOWS_LIVE_LOGS_DRAINED_MARKER);
219 let stdout_source = rootfs.join(WINDOWS_GUEST_STDOUT);
220 let stderr_source = rootfs.join(WINDOWS_GUEST_STDERR);
221
222 if !windows_marker_matches(&marker, b"collected\n") {
223 std::fs::create_dir_all(&logs)?;
224 let runtime_filter = a3s_box_core::log::RuntimeConsoleFilter::new();
225
226 for (source, destination) in [
227 (&stdout_source, logs.join("console.log")),
228 (&stderr_source, logs.join("console.err.log")),
229 ] {
230 append_windows_guest_stream(source, &destination, &runtime_filter).map_err(
231 |error| BoxError::BoxBootError {
232 message: format!(
233 "Failed to collect Windows guest output {} into {}: {error}",
234 source.display(),
235 destination.display()
236 ),
237 hint: None,
238 },
239 )?;
240 }
241
242 if !windows_marker_matches(&live_logs_drained, b"drained\n") {
248 let stopped = std::sync::atomic::AtomicBool::new(true);
249 a3s_box_core::log::run_log_processor_streams(
250 &stdout_source,
251 &stderr_source,
252 &logs,
253 log_config,
254 &stopped,
255 );
256 }
257
258 a3s_box_core::windows_file::replace_regular_file(&marker, b"collected\n").map_err(
259 |error| BoxError::BoxBootError {
260 message: format!(
261 "Failed to mark the Windows guest result collected at {}: {error}",
262 marker.display()
263 ),
264 hint: None,
265 },
266 )?;
267 }
268
269 let exit_path = rootfs.join(WINDOWS_GUEST_EXIT_CODE);
270 let contents = match a3s_box_core::windows_file::open_regular_file(&exit_path, None) {
271 Ok((file, _)) => {
272 use std::io::Read;
273 let mut contents = String::new();
274 file.take(64)
275 .read_to_string(&mut contents)
276 .map_err(|error| BoxError::BoxBootError {
277 message: format!(
278 "Failed to read the Windows guest exit code {}: {error}",
279 exit_path.display()
280 ),
281 hint: None,
282 })?;
283 contents
284 }
285 Err(error) if error.kind() == std::io::ErrorKind::NotFound && shim_exit_code != 0 => {
286 return Ok(shim_exit_code);
287 }
288 Err(error) => {
289 return Err(BoxError::BoxBootError {
290 message: if error.kind() == std::io::ErrorKind::NotFound {
291 format!(
292 "WHPX stopped before the guest persisted its exit code ({})",
293 exit_path.display()
294 )
295 } else {
296 format!(
297 "Failed to read the Windows guest exit code {}: {error}",
298 exit_path.display()
299 )
300 },
301 hint: Some(
302 "Inspect logs/init-rust.log and the shim log for the guest boot failure"
303 .to_string(),
304 ),
305 });
306 }
307 };
308
309 contents
310 .trim()
311 .parse::<i32>()
312 .map_err(|error| BoxError::BoxBootError {
313 message: format!(
314 "Invalid Windows guest exit code in {}: {error}",
315 exit_path.display()
316 ),
317 hint: None,
318 })
319}
320
321pub struct VmManager {
323 pub(crate) config: BoxConfig,
325
326 pub(crate) box_id: String,
328
329 boot_mode: VmBootMode,
331
332 pub(crate) state: Arc<RwLock<BoxState>>,
334
335 pub(crate) event_emitter: EventEmitter,
337
338 pub(crate) provider: Option<Box<dyn VmmProvider>>,
340
341 pub(crate) handler: Arc<RwLock<Option<Box<dyn VmHandler>>>>,
343
344 #[cfg(unix)]
346 pub(crate) exec_client: Option<ExecClient>,
347
348 pub(crate) net_manager: Option<Box<dyn crate::network::NetworkBackend>>,
351
352 pub(crate) home_dir: PathBuf,
354
355 pub(crate) anonymous_volumes: Vec<String>,
357
358 pub(crate) created_anonymous_volumes: Vec<String>,
363
364 pub(crate) image_config: Option<crate::oci::OciImageConfig>,
366
367 pub(crate) restore_rootfs_cache_key: Option<String>,
372
373 pub(crate) healthcheck_disabled: bool,
376
377 pub(crate) preserve_rootfs_on_boot_failure: bool,
381
382 #[cfg(unix)]
384 pub(crate) tee: Option<Box<dyn TeeExtension>>,
385
386 pub(crate) rootfs_provider: Box<dyn crate::rootfs::RootfsProvider>,
388
389 pub(crate) exec_socket_path: Option<PathBuf>,
391
392 pub(crate) pty_socket_path: Option<PathBuf>,
394
395 pub(crate) port_forward_socket_path: Option<PathBuf>,
397
398 pub(crate) prom: Option<crate::prom::RuntimeMetrics>,
400
401 pub(crate) shim_exit_code: Option<i32>,
403
404 pub(crate) pull_progress_fn: Option<PullProgressFn>,
406
407 pub(crate) log_config: a3s_box_core::log::LogConfig,
411
412 pub(crate) resolved_execution_plan: Option<ResolvedExecutionPlan>,
414
415 pub(crate) managed_secret_root: Option<PathBuf>,
418
419 pub(crate) transient_registry_auth: Option<crate::oci::RegistryAuth>,
422}
423
424impl VmManager {
425 pub fn new(config: BoxConfig, event_emitter: EventEmitter) -> Self {
427 let box_id = uuid::Uuid::new_v4().to_string();
428 let home_dir = a3s_box_core::dirs_home();
429 let rootfs_provider =
430 crate::rootfs::default_provider_for_boot(rootfs_snapshot_requested(&config));
431
432 Self {
433 config,
434 box_id,
435 boot_mode: VmBootMode::Workload,
436 state: Arc::new(RwLock::new(BoxState::Created)),
437 event_emitter,
438 provider: None,
439 handler: Arc::new(RwLock::new(None)),
440 #[cfg(unix)]
441 exec_client: None,
442 net_manager: None,
443 home_dir,
444 anonymous_volumes: Vec::new(),
445 created_anonymous_volumes: Vec::new(),
446 image_config: None,
447 restore_rootfs_cache_key: None,
448 healthcheck_disabled: false,
449 preserve_rootfs_on_boot_failure: false,
450 #[cfg(unix)]
451 tee: None,
452 rootfs_provider,
453 exec_socket_path: None,
454 pty_socket_path: None,
455 port_forward_socket_path: None,
456 prom: None,
457 shim_exit_code: None,
458 pull_progress_fn: None,
459 log_config: a3s_box_core::log::LogConfig::default(),
460 resolved_execution_plan: None,
461 managed_secret_root: None,
462 transient_registry_auth: None,
463 }
464 }
465
466 pub fn with_box_id(config: BoxConfig, event_emitter: EventEmitter, box_id: String) -> Self {
468 let home_dir = a3s_box_core::dirs_home();
469 let rootfs_provider = crate::rootfs::default_provider_for_box_boot(
470 &home_dir.join("boxes").join(&box_id),
471 rootfs_snapshot_requested(&config),
472 );
473
474 Self {
475 config,
476 box_id,
477 boot_mode: VmBootMode::Workload,
478 state: Arc::new(RwLock::new(BoxState::Created)),
479 event_emitter,
480 provider: None,
481 handler: Arc::new(RwLock::new(None)),
482 #[cfg(unix)]
483 exec_client: None,
484 net_manager: None,
485 home_dir,
486 anonymous_volumes: Vec::new(),
487 created_anonymous_volumes: Vec::new(),
488 image_config: None,
489 restore_rootfs_cache_key: None,
490 healthcheck_disabled: false,
491 preserve_rootfs_on_boot_failure: false,
492 #[cfg(unix)]
493 tee: None,
494 rootfs_provider,
495 exec_socket_path: None,
496 pty_socket_path: None,
497 port_forward_socket_path: None,
498 prom: None,
499 shim_exit_code: None,
500 pull_progress_fn: None,
501 log_config: a3s_box_core::log::LogConfig::default(),
502 resolved_execution_plan: None,
503 managed_secret_root: None,
504 transient_registry_auth: None,
505 }
506 }
507
508 async fn cleanup_boot_failure(&mut self) {
510 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
511
512 #[cfg(target_os = "windows")]
513 let guest_exit_before_cleanup = windows_guest_persisted_exit_code(&box_dir);
514
515 if let Some(mut handler) = self.handler.write().await.take() {
516 let exited_before_cleanup = handler.has_exited();
527 let collected_before_cleanup = match handler.try_wait_exit() {
528 Ok(Some(exit_code)) => {
529 self.shim_exit_code = Some(exit_code);
530 true
531 }
532 Ok(None) => false,
533 Err(error) => {
534 tracing::debug!(
535 box_id = %self.box_id,
536 error = %error,
537 "Failed to collect a terminal status before boot cleanup"
538 );
539 false
540 }
541 };
542 if let Err(error) = handler.stop(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS) {
543 tracing::warn!(
544 box_id = %self.box_id,
545 error = %error,
546 "Failed to stop VM handler after boot failure"
547 );
548 }
549 let provider_exit_code = handler.exit_code().or(self.shim_exit_code);
550 #[cfg(not(target_os = "windows"))]
551 {
552 self.shim_exit_code =
553 crate::rootfs::resolve_workload_exit_code(&box_dir, provider_exit_code);
554 }
555 #[cfg(target_os = "windows")]
556 {
557 let completed_before_cleanup = collected_before_cleanup
558 || exited_before_cleanup
559 || guest_exit_before_cleanup.is_some();
560 if completed_before_cleanup {
561 let fallback_exit_code = guest_exit_before_cleanup.or(provider_exit_code);
562 if let Some(fallback_exit_code) = fallback_exit_code {
563 match collect_windows_guest_result(
564 &box_dir,
565 &self.log_config,
566 fallback_exit_code,
567 ) {
568 Ok(exit_code) => self.shim_exit_code = Some(exit_code),
569 Err(error) => {
570 tracing::warn!(
571 box_id = %self.box_id,
572 error = %error,
573 "Failed to collect the completed Windows guest during boot cleanup"
574 );
575 self.shim_exit_code =
576 guest_exit_before_cleanup.or(provider_exit_code);
577 }
578 }
579 } else {
580 self.shim_exit_code = None;
584 }
585 } else {
586 self.shim_exit_code = provider_exit_code;
587 }
588 }
589 if exited_before_cleanup && self.shim_exit_code.is_none() {
590 self.shim_exit_code =
591 wait_for_delayed_terminal_exit(handler.as_mut(), &box_dir, &self.box_id).await;
592 }
593 let completed_before_cleanup = collected_before_cleanup || exited_before_cleanup;
594 #[cfg(target_os = "windows")]
595 let completed_before_cleanup =
596 completed_before_cleanup || guest_exit_before_cleanup.is_some();
597 if self.config.persistent && self.shim_exit_code.is_some() && completed_before_cleanup {
598 self.preserve_rootfs_on_boot_failure = true;
599 }
600 }
601
602 if let Some(mut net_manager) = self.net_manager.take() {
603 net_manager.stop();
604 }
605
606 self.cleanup_created_anonymous_volumes();
607 self.cleanup_box_dir();
608 }
609
610 fn cleanup_created_anonymous_volumes(&mut self) {
611 if self.created_anonymous_volumes.is_empty() {
612 return;
613 }
614
615 let created = std::mem::take(&mut self.created_anonymous_volumes);
616 let created_set: std::collections::HashSet<_> = created.iter().cloned().collect();
617 let store = crate::volume::VolumeStore::new(
618 self.home_dir.join("volumes.json"),
619 self.home_dir.join("volumes"),
620 );
621
622 for volume_name in &created {
623 if let Err(error) = store.remove_anonymous(volume_name, &self.box_id) {
624 tracing::debug!(
625 box_id = %self.box_id,
626 volume = volume_name,
627 error = %error,
628 "Failed to remove anonymous volume after boot failure"
629 );
630 }
631 }
632
633 self.anonymous_volumes
634 .retain(|name| !created_set.contains(name));
635 }
636
637 fn cleanup_box_dir(&self) {
639 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
640 let socket_dir = self.socket_dir();
641 let mount_aliases_clean = match self.cleanup_sandbox_mount_aliases() {
642 Ok(()) => true,
643 Err(error) => {
644 tracing::warn!(
645 box_id = %self.box_id,
646 %error,
647 "Failed to cleanup Sandbox attachment aliases after boot failure"
648 );
649 false
650 }
651 };
652
653 #[cfg(target_os = "linux")]
660 crate::network::terminate_passt(&self.socket_dir());
661
662 let preserve_rootfs_on_boot_failure = self.preserve_rootfs_on_boot_failure
663 || self.rootfs_provider.preserve_on_boot_failure(&box_dir);
664 if let Err(error) = self
665 .rootfs_provider
666 .cleanup(&box_dir, preserve_rootfs_on_boot_failure)
667 {
668 tracing::warn!(
669 box_id = %self.box_id,
670 path = %box_dir.display(),
671 error = %error,
672 "Failed to cleanup rootfs provider after boot failure"
673 );
674 }
675
676 match std::fs::remove_dir_all(&socket_dir) {
677 Ok(()) => {}
678 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
679 Err(error) => {
680 tracing::debug!(
681 box_id = %self.box_id,
682 path = %socket_dir.display(),
683 error = %error,
684 "Failed to cleanup socket directory after boot failure"
685 );
686 }
687 }
688
689 if !self.config.persistent && mount_aliases_clean {
693 match std::fs::remove_dir_all(&box_dir) {
694 Ok(()) => {}
695 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
696 Err(error) => {
697 tracing::warn!(
698 box_id = %self.box_id,
699 path = %box_dir.display(),
700 error = %error,
701 "Failed to cleanup box directory after boot failure"
702 );
703 }
704 }
705 }
706 }
707
708 fn cleanup_sandbox_mount_aliases(&self) -> Result<()> {
709 if self.config.isolation.is_sandbox() {
710 crate::sandbox::cleanup_sandbox_mount_aliases(&self.home_dir, &self.box_id)
711 } else {
712 Ok(())
713 }
714 }
715
716 pub fn with_provider(
718 config: BoxConfig,
719 event_emitter: EventEmitter,
720 provider: Box<dyn VmmProvider>,
721 ) -> Self {
722 let box_id = uuid::Uuid::new_v4().to_string();
723 let home_dir = a3s_box_core::dirs_home();
724 let rootfs_provider =
725 crate::rootfs::default_provider_for_boot(rootfs_snapshot_requested(&config));
726 Self {
727 config,
728 box_id,
729 boot_mode: VmBootMode::Workload,
730 state: Arc::new(RwLock::new(BoxState::Created)),
731 event_emitter,
732 provider: Some(provider),
733 handler: Arc::new(RwLock::new(None)),
734 #[cfg(unix)]
735 exec_client: None,
736 net_manager: None,
737 home_dir,
738 anonymous_volumes: Vec::new(),
739 created_anonymous_volumes: Vec::new(),
740 image_config: None,
741 restore_rootfs_cache_key: None,
742 healthcheck_disabled: false,
743 preserve_rootfs_on_boot_failure: false,
744 #[cfg(unix)]
745 tee: None,
746 rootfs_provider,
747 exec_socket_path: None,
748 pty_socket_path: None,
749 port_forward_socket_path: None,
750 prom: None,
751 shim_exit_code: None,
752 pull_progress_fn: None,
753 log_config: a3s_box_core::log::LogConfig::default(),
754 resolved_execution_plan: None,
755 managed_secret_root: None,
756 transient_registry_auth: None,
757 }
758 }
759
760 pub fn box_id(&self) -> &str {
762 &self.box_id
763 }
764
765 pub async fn state(&self) -> BoxState {
767 *self.state.read().await
768 }
769}
770
771fn rootfs_snapshot_requested(config: &BoxConfig) -> bool {
777 if config.snapshot_mem_file.is_some()
778 || config.snapshot_sock.is_some()
779 || config.restore_from.is_some()
780 {
781 return true;
782 }
783
784 #[cfg(unix)]
785 {
786 std::env::var_os("KRUN_RESTORE_FROM").is_some_and(|value| !value.as_os_str().is_empty())
787 }
788
789 #[cfg(not(unix))]
790 {
791 false
792 }
793}
794
795pub(crate) const fn native_snapshot_fork_supported() -> bool {
802 cfg!(all(target_os = "linux", target_arch = "x86_64"))
803}
804
805fn validate_snapshot_launch(config: &BoxConfig) -> Result<()> {
806 let memory = config
807 .snapshot_mem_file
808 .as_deref()
809 .filter(|value| !value.is_empty())
810 .map(str::to_owned)
811 .or_else(|| snapshot_env_nonempty("KRUN_SNAPSHOT_MEM_FILE"));
812 let trigger = config
813 .snapshot_sock
814 .as_deref()
815 .filter(|value| !value.is_empty())
816 .map(str::to_owned)
817 .or_else(|| snapshot_env_nonempty("KRUN_SNAPSHOT_SOCK"));
818 let restore = config
819 .restore_from
820 .as_deref()
821 .filter(|value| !value.is_empty())
822 .map(str::to_owned)
823 .or_else(|| snapshot_env_nonempty("KRUN_RESTORE_FROM"));
824
825 validate_snapshot_launch_shape(memory.is_some(), trigger.is_some(), restore.is_some())
826}
827
828fn validate_snapshot_launch_shape(memory: bool, trigger: bool, restore: bool) -> Result<()> {
829 let requested = memory || trigger || restore;
830 if !requested {
831 return Ok(());
832 }
833 if !native_snapshot_fork_supported() {
834 return Err(BoxError::ConfigError(
835 "native VM snapshot-fork is supported only by the Linux x86_64 KVM build".to_string(),
836 ));
837 }
838 match (memory, trigger, restore) {
839 (true, true, false) | (true, false, true) => Ok(()),
840 _ => Err(BoxError::ConfigError(
841 "invalid native VM snapshot configuration: template mode requires memory + trigger socket, while restore mode requires memory + state file"
842 .to_string(),
843 )),
844 }
845}
846
847fn snapshot_env_nonempty(name: &str) -> Option<String> {
848 std::env::var(name).ok().filter(|value| !value.is_empty())
849}
850
851#[cfg(unix)]
856fn is_restore_mode(config: &BoxConfig) -> bool {
857 config
858 .restore_from
859 .as_deref()
860 .is_some_and(|s| !s.is_empty())
861 || std::env::var("KRUN_RESTORE_FROM")
862 .map(|v| !v.is_empty())
863 .unwrap_or(false)
864}
865
866pub(crate) fn fnv1a_hash(input: &str) -> u64 {
868 let mut hash: u64 = 0xcbf29ce484222325;
869 for byte in input.bytes() {
870 hash ^= byte as u64;
871 hash = hash.wrapping_mul(0x100000001b3);
872 }
873 hash
874}
875
876#[cfg(unix)]
877fn default_stop_signal() -> i32 {
878 libc::SIGTERM
879}
880
881#[cfg(windows)]
882fn default_stop_signal() -> i32 {
883 15
884}
885
886async fn wait_for_delayed_terminal_exit(
887 handler: &mut dyn VmHandler,
888 box_dir: &Path,
889 box_id: &str,
890) -> Option<i32> {
891 let deadline = tokio::time::Instant::now() + TERMINAL_EXIT_POLL_TIMEOUT;
892 let mut reported_wait_error = false;
893 loop {
894 if let Some(exit_code) = handler.exit_code() {
895 return Some(exit_code);
896 }
897 if let Some(exit_code) = boot_failure_persisted_exit_code(box_dir) {
898 return Some(exit_code);
899 }
900 match handler.try_wait_exit() {
901 Ok(Some(exit_code)) => return Some(exit_code),
902 Ok(None) => {}
903 Err(error) => {
904 if !reported_wait_error {
905 tracing::debug!(
906 %box_id,
907 %error,
908 "Terminal status remained unavailable after boot cleanup"
909 );
910 reported_wait_error = true;
911 }
912 }
913 }
914 if tokio::time::Instant::now() >= deadline {
915 return None;
916 }
917 tokio::time::sleep(TERMINAL_EXIT_POLL_INTERVAL).await;
918 }
919}
920
921#[cfg(not(target_os = "windows"))]
922fn boot_failure_persisted_exit_code(box_dir: &Path) -> Option<i32> {
923 crate::rootfs::read_persisted_exit_code(box_dir)
924}
925
926#[cfg(target_os = "windows")]
927fn boot_failure_persisted_exit_code(box_dir: &Path) -> Option<i32> {
928 windows_guest_persisted_exit_code(box_dir)
929}
930
931#[cfg(test)]
932#[path = "tests.rs"]
933mod tests;