1mod layout;
4mod network;
5mod ready;
6pub mod reap;
7mod spec;
8
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12type PullProgressFn = Arc<dyn Fn(usize, usize, &str, i64) + Send + Sync>;
14
15use a3s_box_core::config::BoxConfig;
16#[cfg(unix)]
17use a3s_box_core::config::TeeConfig;
18use a3s_box_core::error::{BoxError, Result};
19use a3s_box_core::event::{BoxEvent, EventEmitter};
20use serde::{Deserialize, Serialize};
21use tokio::sync::RwLock;
22use tracing::Instrument;
23
24#[cfg(unix)]
25use libc;
26
27#[cfg(unix)]
28use crate::grpc::ExecClient;
29#[cfg(unix)]
30use crate::tee::TeeExtension;
31use crate::vmm::{VmController, VmHandler, VmmProvider, DEFAULT_SHUTDOWN_TIMEOUT_MS};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum BoxState {
36 Created,
38
39 Ready,
41
42 Busy,
44
45 Compacting,
47
48 Stopped,
50}
51
52pub(crate) struct BoxLayout {
54 pub(crate) rootfs_path: PathBuf,
56 pub(crate) exec_socket_path: PathBuf,
58 pub(crate) pty_socket_path: PathBuf,
60 pub(crate) attest_socket_path: PathBuf,
62 pub(crate) port_forward_socket_path: PathBuf,
64 pub(crate) workspace_path: PathBuf,
66 pub(crate) console_output: Option<PathBuf>,
68 pub(crate) oci_config: Option<crate::oci::OciImageConfig>,
70 pub(crate) tee_instance_config: Option<crate::vmm::TeeInstanceConfig>,
72}
73
74pub struct VmManager {
76 pub(crate) config: BoxConfig,
78
79 pub(crate) box_id: String,
81
82 pub(crate) state: Arc<RwLock<BoxState>>,
84
85 pub(crate) event_emitter: EventEmitter,
87
88 pub(crate) provider: Option<Box<dyn VmmProvider>>,
90
91 pub(crate) handler: Arc<RwLock<Option<Box<dyn VmHandler>>>>,
93
94 #[cfg(unix)]
96 pub(crate) exec_client: Option<ExecClient>,
97
98 pub(crate) net_manager: Option<Box<dyn crate::network::NetworkBackend>>,
101
102 pub(crate) home_dir: PathBuf,
104
105 pub(crate) anonymous_volumes: Vec<String>,
107
108 pub(crate) created_anonymous_volumes: Vec<String>,
113
114 pub(crate) image_config: Option<crate::oci::OciImageConfig>,
116
117 #[cfg(unix)]
119 pub(crate) tee: Option<Box<dyn TeeExtension>>,
120
121 pub(crate) rootfs_provider: Box<dyn crate::rootfs::RootfsProvider>,
123
124 pub(crate) exec_socket_path: Option<PathBuf>,
126
127 pub(crate) pty_socket_path: Option<PathBuf>,
129
130 pub(crate) port_forward_socket_path: Option<PathBuf>,
132
133 pub(crate) prom: Option<crate::prom::RuntimeMetrics>,
135
136 pub(crate) shim_exit_code: Option<i32>,
138
139 pub(crate) pull_progress_fn: Option<PullProgressFn>,
141
142 pub(crate) log_config: a3s_box_core::log::LogConfig,
146}
147
148impl VmManager {
149 pub fn new(config: BoxConfig, event_emitter: EventEmitter) -> Self {
151 let box_id = uuid::Uuid::new_v4().to_string();
152 let home_dir = a3s_box_core::dirs_home();
153
154 Self {
155 config,
156 box_id,
157 state: Arc::new(RwLock::new(BoxState::Created)),
158 event_emitter,
159 provider: None,
160 handler: Arc::new(RwLock::new(None)),
161 #[cfg(unix)]
162 exec_client: None,
163 net_manager: None,
164 home_dir,
165 anonymous_volumes: Vec::new(),
166 created_anonymous_volumes: Vec::new(),
167 image_config: None,
168 #[cfg(unix)]
169 tee: None,
170 rootfs_provider: crate::rootfs::default_provider(),
171 exec_socket_path: None,
172 pty_socket_path: None,
173 port_forward_socket_path: None,
174 prom: None,
175 shim_exit_code: None,
176 pull_progress_fn: None,
177 log_config: a3s_box_core::log::LogConfig::default(),
178 }
179 }
180
181 pub fn with_box_id(config: BoxConfig, event_emitter: EventEmitter, box_id: String) -> Self {
183 let home_dir = a3s_box_core::dirs_home();
184
185 Self {
186 config,
187 box_id,
188 state: Arc::new(RwLock::new(BoxState::Created)),
189 event_emitter,
190 provider: None,
191 handler: Arc::new(RwLock::new(None)),
192 #[cfg(unix)]
193 exec_client: None,
194 net_manager: None,
195 home_dir,
196 anonymous_volumes: Vec::new(),
197 created_anonymous_volumes: Vec::new(),
198 image_config: None,
199 #[cfg(unix)]
200 tee: None,
201 rootfs_provider: crate::rootfs::default_provider(),
202 exec_socket_path: None,
203 pty_socket_path: None,
204 port_forward_socket_path: None,
205 prom: None,
206 shim_exit_code: None,
207 pull_progress_fn: None,
208 log_config: a3s_box_core::log::LogConfig::default(),
209 }
210 }
211
212 async fn cleanup_boot_failure(&mut self) {
214 if let Some(mut handler) = self.handler.write().await.take() {
215 if let Err(error) = handler.stop(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS) {
216 tracing::warn!(
217 box_id = %self.box_id,
218 error = %error,
219 "Failed to stop VM handler after boot failure"
220 );
221 }
222 self.shim_exit_code = handler.exit_code();
223 }
224
225 if let Some(mut net_manager) = self.net_manager.take() {
226 net_manager.stop();
227 }
228
229 self.cleanup_created_anonymous_volumes();
230 self.cleanup_box_dir();
231 }
232
233 fn cleanup_created_anonymous_volumes(&mut self) {
234 if self.created_anonymous_volumes.is_empty() {
235 return;
236 }
237
238 let created = std::mem::take(&mut self.created_anonymous_volumes);
239 let created_set: std::collections::HashSet<_> = created.iter().cloned().collect();
240 let store = crate::volume::VolumeStore::new(
241 self.home_dir.join("volumes.json"),
242 self.home_dir.join("volumes"),
243 );
244
245 for volume_name in &created {
246 if let Err(error) = store.remove(volume_name, true) {
247 tracing::debug!(
248 box_id = %self.box_id,
249 volume = volume_name,
250 error = %error,
251 "Failed to remove anonymous volume after boot failure"
252 );
253 }
254 }
255
256 self.anonymous_volumes
257 .retain(|name| !created_set.contains(name));
258 }
259
260 fn cleanup_box_dir(&self) {
262 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
263
264 #[cfg(target_os = "linux")]
271 crate::network::terminate_passt(&self.socket_dir());
272
273 if let Err(error) = self.rootfs_provider.cleanup(&box_dir, false) {
274 tracing::warn!(
275 box_id = %self.box_id,
276 path = %box_dir.display(),
277 error = %error,
278 "Failed to cleanup rootfs provider after boot failure"
279 );
280 }
281
282 match std::fs::remove_dir_all(&box_dir) {
283 Ok(()) => {}
284 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
285 Err(error) => {
286 tracing::warn!(
287 box_id = %self.box_id,
288 path = %box_dir.display(),
289 error = %error,
290 "Failed to cleanup box directory after boot failure"
291 );
292 }
293 }
294
295 let socket_dir = self.socket_dir();
296 if socket_dir != box_dir.join("sockets") {
297 match std::fs::remove_dir_all(&socket_dir) {
298 Ok(()) => {}
299 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
300 Err(error) => {
301 tracing::debug!(
302 box_id = %self.box_id,
303 path = %socket_dir.display(),
304 error = %error,
305 "Failed to cleanup socket directory after boot failure"
306 );
307 }
308 }
309 }
310 }
311
312 pub fn with_provider(
314 config: BoxConfig,
315 event_emitter: EventEmitter,
316 provider: Box<dyn VmmProvider>,
317 ) -> Self {
318 let box_id = uuid::Uuid::new_v4().to_string();
319 let home_dir = a3s_box_core::dirs_home();
320 Self {
321 config,
322 box_id,
323 state: Arc::new(RwLock::new(BoxState::Created)),
324 event_emitter,
325 provider: Some(provider),
326 handler: Arc::new(RwLock::new(None)),
327 #[cfg(unix)]
328 exec_client: None,
329 net_manager: None,
330 home_dir,
331 anonymous_volumes: Vec::new(),
332 created_anonymous_volumes: Vec::new(),
333 image_config: None,
334 #[cfg(unix)]
335 tee: None,
336 rootfs_provider: crate::rootfs::default_provider(),
337 exec_socket_path: None,
338 pty_socket_path: None,
339 port_forward_socket_path: None,
340 prom: None,
341 shim_exit_code: None,
342 pull_progress_fn: None,
343 log_config: a3s_box_core::log::LogConfig::default(),
344 }
345 }
346
347 pub fn box_id(&self) -> &str {
349 &self.box_id
350 }
351
352 pub async fn state(&self) -> BoxState {
354 *self.state.read().await
355 }
356
357 #[cfg(unix)]
359 pub fn exec_client(&self) -> Option<&ExecClient> {
360 self.exec_client.as_ref()
361 }
362
363 #[cfg(unix)]
369 pub async fn attach_running_process(
370 &mut self,
371 pid: u32,
372 exec_socket_path: PathBuf,
373 pty_socket_path: Option<PathBuf>,
374 ) -> Result<()> {
375 let port_forward_socket_path = exec_socket_path.with_file_name("portfwd.sock");
376 let handler = crate::vmm::ShimHandler::from_pid(pid, self.box_id.clone());
377 if !handler.is_running() {
378 return Err(BoxError::StateError(format!(
379 "Cannot attach to non-running VM process {pid}"
380 )));
381 }
382
383 self.exec_client = match ExecClient::connect(&exec_socket_path).await {
384 Ok(client) => Some(client),
385 Err(error) => {
386 tracing::debug!(
387 box_id = %self.box_id,
388 socket_path = %exec_socket_path.display(),
389 error = %error,
390 "Failed to reconnect exec client while attaching to running VM"
391 );
392 None
393 }
394 };
395 self.exec_socket_path = Some(exec_socket_path);
396 self.pty_socket_path = pty_socket_path;
397 self.port_forward_socket_path = Some(port_forward_socket_path);
398 *self.handler.write().await = Some(Box::new(handler));
399 *self.state.write().await = BoxState::Ready;
400 Ok(())
401 }
402
403 pub fn exec_socket_path(&self) -> Option<&Path> {
405 self.exec_socket_path.as_deref()
406 }
407
408 pub fn pty_socket_path(&self) -> Option<&Path> {
410 self.pty_socket_path.as_deref()
411 }
412
413 pub fn port_forward_socket_path(&self) -> Option<&Path> {
415 self.port_forward_socket_path.as_deref()
416 }
417
418 pub fn set_provider(&mut self, provider: Box<dyn VmmProvider>) {
423 self.provider = Some(provider);
424 }
425
426 pub fn set_rootfs_provider(&mut self, provider: Box<dyn crate::rootfs::RootfsProvider>) {
431 self.rootfs_provider = provider;
432 }
433
434 pub fn rootfs_provider_name(&self) -> &str {
436 self.rootfs_provider.name()
437 }
438
439 pub fn set_pull_progress_fn(&mut self, f: PullProgressFn) {
442 self.pull_progress_fn = Some(f);
443 }
444
445 pub fn set_metrics(&mut self, metrics: crate::prom::RuntimeMetrics) {
447 self.prom = Some(metrics);
448 }
449
450 pub fn set_log_config(&mut self, log_config: a3s_box_core::log::LogConfig) {
453 self.log_config = log_config;
454 }
455
456 pub fn metrics_prom(&self) -> Option<&crate::prom::RuntimeMetrics> {
458 self.prom.as_ref()
459 }
460
461 pub fn anonymous_volumes(&self) -> &[String] {
466 &self.anonymous_volumes
467 }
468
469 pub fn image_config(&self) -> Option<&crate::oci::OciImageConfig> {
471 self.image_config.as_ref()
472 }
473
474 pub fn exit_code(&self) -> Option<i32> {
480 self.shim_exit_code
481 }
482
483 fn persisted_exit_code(&self) -> Option<i32> {
484 std::fs::read_to_string(
485 self.home_dir
486 .join("boxes")
487 .join(&self.box_id)
488 .join("upper")
489 .join(".a3s_exit_code"),
490 )
491 .ok()
492 .and_then(|contents| contents.trim().parse::<i32>().ok())
493 }
494
495 pub async fn try_wait_exit(&mut self) -> Result<Option<i32>> {
501 if let Some(code) = self.persisted_exit_code() {
502 self.shim_exit_code = Some(code);
503 return Ok(Some(code));
504 }
505
506 let mut handler = self.handler.write().await;
507 let Some(handler) = handler.as_mut() else {
508 return Ok(self.shim_exit_code);
509 };
510
511 if let Some(code) = handler.try_wait_exit()? {
512 self.shim_exit_code = Some(code);
513 return Ok(Some(code));
514 }
515
516 Ok(None)
517 }
518
519 pub async fn has_exited(&self) -> bool {
522 if self.shim_exit_code.is_some() || self.persisted_exit_code().is_some() {
523 return true;
524 }
525
526 self.handler
527 .read()
528 .await
529 .as_ref()
530 .map(|handler| handler.has_exited())
531 .unwrap_or(false)
532 }
533
534 #[cfg(unix)]
542 pub async fn run_deferred_main(
543 &mut self,
544 spec_json: &[u8],
545 timeout: std::time::Duration,
546 ) -> Result<a3s_box_core::exec::ExecOutput> {
547 let acked = {
548 let client = self
549 .exec_client
550 .as_ref()
551 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
552 client.spawn_main(Some(spec_json)).await?
553 };
554 if !acked {
555 return Err(BoxError::ExecError(
556 "spawn-main was not acknowledged by the guest".to_string(),
557 ));
558 }
559
560 let start = std::time::Instant::now();
562 let exit_code = loop {
563 if let Some(code) = self.try_wait_exit().await? {
564 break code;
565 }
566 if start.elapsed() >= timeout {
567 return Err(BoxError::ExecError(
568 "deferred main did not exit within the timeout".to_string(),
569 ));
570 }
571 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
572 };
573
574 let json_path = self
579 .home_dir
580 .join("boxes")
581 .join(&self.box_id)
582 .join("logs")
583 .join("container.json");
584 let drain_start = std::time::Instant::now();
585 let mut last_len = u64::MAX;
586 loop {
587 let len = std::fs::metadata(&json_path).map(|m| m.len()).unwrap_or(0);
588 if len == last_len || drain_start.elapsed() >= std::time::Duration::from_secs(1) {
589 break;
590 }
591 last_len = len;
592 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
593 }
594 let (stdout, stderr) = self.read_container_logs();
595 Ok(a3s_box_core::exec::ExecOutput {
596 stdout,
597 stderr,
598 exit_code,
599 })
600 }
601
602 fn read_container_logs(&self) -> (Vec<u8>, Vec<u8>) {
604 let path = self
605 .home_dir
606 .join("boxes")
607 .join(&self.box_id)
608 .join("logs")
609 .join("container.json");
610 let (mut out, mut err) = (Vec::new(), Vec::new());
611 if let Ok(content) = std::fs::read_to_string(&path) {
612 for line in content.lines() {
613 if let Ok(entry) = serde_json::from_str::<a3s_box_core::log::LogEntry>(line) {
614 if entry.stream == "stderr" {
615 err.extend_from_slice(entry.log.as_bytes());
616 } else {
617 out.extend_from_slice(entry.log.as_bytes());
618 }
619 }
620 }
621 }
622 (out, err)
623 }
624
625 #[cfg(unix)]
629 #[tracing::instrument(skip(self, request), fields(box_id = %self.box_id))]
630 pub async fn exec_request(
631 &self,
632 request: &a3s_box_core::exec::ExecRequest,
633 ) -> Result<a3s_box_core::exec::ExecOutput> {
634 if request.cmd.is_empty() {
635 return Err(BoxError::ExecError(
636 "Exec request requires a non-empty command".to_string(),
637 ));
638 }
639
640 let state = self.state.read().await;
641 match *state {
642 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
643 BoxState::Created => {
644 return Err(BoxError::ExecError("VM not yet booted".to_string()));
645 }
646 BoxState::Stopped => {
647 return Err(BoxError::ExecError("VM is stopped".to_string()));
648 }
649 }
650 drop(state);
651
652 let client = self
653 .exec_client
654 .as_ref()
655 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
656
657 let exec_start = std::time::Instant::now();
658 let result = client.exec_command(request).await;
659
660 if let Some(ref prom) = self.prom {
662 prom.exec_total.inc();
663 prom.exec_duration
664 .observe(exec_start.elapsed().as_secs_f64());
665 if result.is_err() || result.as_ref().is_ok_and(|o| o.exit_code != 0) {
666 prom.exec_errors_total.inc();
667 }
668 }
669
670 result
671 }
672
673 #[cfg(unix)]
677 #[tracing::instrument(skip(self, cmd), fields(box_id = %self.box_id))]
678 pub async fn exec_command(
679 &self,
680 cmd: Vec<String>,
681 timeout_ns: u64,
682 ) -> Result<a3s_box_core::exec::ExecOutput> {
683 let request = a3s_box_core::exec::ExecRequest {
684 cmd,
685 timeout_ns,
686 env: vec![],
687 working_dir: None,
688 rootfs: None,
689 stdin: None,
690 stdin_streaming: false,
691 user: None,
692 streaming: false,
693 };
694
695 self.exec_request(&request).await
696 }
697
698 pub async fn boot(&mut self) -> Result<()> {
700 let boot_span = tracing::info_span!("vm_boot", box_id = %self.box_id);
701 {
703 let state = self.state.read().await;
704 if *state != BoxState::Created {
705 return Err(BoxError::StateError("VM already booted".to_string()));
706 }
707 }
708
709 let boot_start = std::time::Instant::now();
710
711 tracing::info!(parent: &boot_span, box_id = %self.box_id, "Booting VM");
712
713 let layout = match self
715 .prepare_layout()
716 .instrument(tracing::info_span!(parent: &boot_span, "prepare_layout"))
717 .await
718 {
719 Ok(layout) => layout,
720 Err(error) => {
721 self.cleanup_boot_failure().await;
722 return Err(error);
723 }
724 };
725 self.image_config = layout.oci_config.clone();
726
727 let resolv_content = a3s_box_core::dns::generate_resolv_conf(&self.config.dns);
729 let resolv_path = layout.rootfs_path.join("etc/resolv.conf");
730 if let Err(e) = tokio::fs::write(&resolv_path, &resolv_content).await {
731 self.cleanup_boot_failure().await;
732 return Err(BoxError::IoError(e));
733 }
734 tracing::debug!(parent: &boot_span, dns = %resolv_content.trim(), "Configured guest DNS");
735
736 if let Err(e) = self.write_hostname_file(&layout) {
738 self.cleanup_boot_failure().await;
739 return Err(e);
740 }
741 if let Err(e) = self.write_standalone_hosts_file(&layout) {
742 self.cleanup_boot_failure().await;
743 return Err(e);
744 }
745
746 let mut spec = match self.build_instance_spec(&layout) {
748 Ok(s) => s,
749 Err(e) => {
750 self.cleanup_boot_failure().await;
751 return Err(e);
752 }
753 };
754
755 let bridge_network = match &self.config.network {
757 a3s_box_core::NetworkMode::Bridge { network } => Some(network.clone()),
758 _ => None,
759 };
760 if let Some(network_name) = bridge_network {
761 let net_config = match self.setup_bridge_network(&network_name) {
762 Ok(n) => n,
763 Err(e) => {
764 self.cleanup_boot_failure().await;
765 return Err(e);
766 }
767 };
768
769 match self.write_hosts_file(&layout, &network_name) {
771 Ok(()) => (),
772 Err(e) => {
773 self.cleanup_boot_failure().await;
774 return Err(e);
775 }
776 };
777
778 let ip_cidr = format!("{}/{}", net_config.ip_address, net_config.prefix_len);
781 spec.entrypoint
782 .env
783 .push(("A3S_NET_IP".to_string(), ip_cidr));
784 spec.entrypoint.env.push((
785 "A3S_NET_GATEWAY".to_string(),
786 net_config.gateway.to_string(),
787 ));
788 spec.entrypoint.env.push((
789 "A3S_NET_DNS".to_string(),
790 net_config
791 .dns_servers
792 .iter()
793 .map(|s| s.to_string())
794 .collect::<Vec<_>>()
795 .join(","),
796 ));
797
798 spec.network = Some(net_config);
799 }
800
801 if self.provider.is_none() {
803 let shim_path = match VmController::find_shim() {
804 Ok(p) => p,
805 Err(e) => {
806 self.cleanup_boot_failure().await;
807 return Err(e);
808 }
809 };
810 let controller = match VmController::new(shim_path) {
811 Ok(c) => c,
812 Err(e) => {
813 self.cleanup_boot_failure().await;
814 return Err(e);
815 }
816 };
817 self.provider = Some(Box::new(controller));
818 }
819
820 let handler = {
822 let provider = self
823 .provider
824 .as_ref()
825 .ok_or_else(|| BoxError::BoxBootError {
826 message: "VMM provider not initialized".to_string(),
827 hint: Some("Ensure VmManager has a provider set before boot".to_string()),
828 })?;
829 let vm_start_span = tracing::info_span!(parent: &boot_span, "vm_start");
830 match async { provider.start(&spec).await }
831 .instrument(vm_start_span)
832 .await
833 {
834 Ok(h) => h,
835 Err(e) => {
836 self.cleanup_boot_failure().await;
837 return Err(e);
838 }
839 }
840 };
841
842 *self.handler.write().await = Some(handler);
844
845 {
847 let wait_span = tracing::info_span!(parent: &boot_span, "wait_for_ready");
848 if let Err(e) = async {
849 self.wait_for_vm_running().await?;
850
851 #[cfg(unix)]
856 if is_restore_mode(&self.config) {
857 self.probe_exec_ready_once(&layout.exec_socket_path).await;
858 } else {
859 self.wait_for_exec_ready(&layout.exec_socket_path).await?;
860 }
861 Ok::<(), BoxError>(())
862 }
863 .instrument(wait_span)
864 .await
865 {
866 self.cleanup_boot_failure().await;
867 return Err(e);
868 }
869 }
870
871 #[cfg(unix)]
882 if !is_restore_mode(&self.config)
883 && std::env::var("BOX_DEFERRED_MAIN")
884 .map(|v| v == "1")
885 .unwrap_or(false)
886 {
887 if let Some(client) = self.exec_client.as_ref() {
888 match client.spawn_main(None).await {
889 Ok(true) => tracing::info!("deferred container main spawned"),
890 Ok(false) => tracing::warn!("deferred spawn-main not acknowledged"),
891 Err(e) => tracing::warn!(error = %e, "deferred spawn-main failed"),
892 }
893 }
894 }
895
896 self.exec_socket_path = Some(layout.exec_socket_path.clone());
898 self.pty_socket_path = Some(layout.pty_socket_path.clone());
899 self.port_forward_socket_path = Some(layout.port_forward_socket_path.clone());
900
901 #[cfg(unix)]
903 if !matches!(self.config.tee, TeeConfig::None) {
904 self.tee = Some(Box::new(crate::tee::SnpTeeExtension::new(
905 self.box_id.clone(),
906 layout.attest_socket_path.clone(),
907 )));
908 }
909
910 *self.state.write().await = BoxState::Ready;
912
913 if let Some(ref prom) = self.prom {
915 let boot_duration = boot_start.elapsed().as_secs_f64();
916 prom.vm_boot_duration.observe(boot_duration);
917 prom.vm_created_total.inc();
918 prom.vm_count.with_label_values(&["ready"]).inc();
919 }
920
921 self.event_emitter.emit(BoxEvent::empty("box.ready"));
923
924 tracing::info!(parent: &boot_span, box_id = %self.box_id, "VM ready");
925
926 Ok(())
927 }
928
929 pub async fn destroy(&mut self) -> Result<()> {
931 self.destroy_with_options(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS)
932 .await
933 }
934
935 pub async fn destroy_with_timeout(&mut self, timeout_ms: u64) -> Result<()> {
937 self.destroy_with_options(libc::SIGTERM, timeout_ms).await
938 }
939
940 #[tracing::instrument(skip(self), fields(box_id = %self.box_id))]
945 pub async fn destroy_with_options(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
946 let mut state = self.state.write().await;
947
948 if *state == BoxState::Stopped {
949 return Ok(());
950 }
951
952 tracing::info!(box_id = %self.box_id, signal, timeout_ms, "Destroying VM");
953
954 *state = BoxState::Stopped;
956
957 let mut stop_error = None;
963 if let Some(mut handler) = self.handler.write().await.take() {
964 if let Err(e) = handler.stop(signal, timeout_ms) {
965 tracing::error!(box_id = %self.box_id, error = %e, "Failed to stop VM handler; continuing teardown");
966 stop_error = Some(e);
967 }
968 self.shim_exit_code = handler.exit_code();
969 }
970
971 if let Some(ref mut net) = self.net_manager {
973 net.stop();
974 }
975 self.net_manager = None;
976
977 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
979 if let Err(e) = self
980 .rootfs_provider
981 .cleanup(&box_dir, self.config.persistent)
982 {
983 tracing::warn!(
984 box_id = %self.box_id,
985 error = %e,
986 "Failed to cleanup rootfs provider"
987 );
988 }
989
990 let socket_dir = self.socket_dir();
991 if let Err(e) = std::fs::remove_dir_all(&socket_dir) {
992 tracing::debug!(
993 box_id = %self.box_id,
994 path = %socket_dir.display(),
995 error = %e,
996 "Failed to cleanup VM socket directory"
997 );
998 }
999
1000 if !self.config.persistent {
1007 match std::fs::remove_dir_all(&box_dir) {
1008 Ok(()) => {}
1009 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
1010 Err(e) => {
1011 tracing::warn!(
1012 box_id = %self.box_id,
1013 path = %box_dir.display(),
1014 error = %e,
1015 "Failed to remove box directory on destroy"
1016 );
1017 }
1018 }
1019 }
1020
1021 if let Some(ref prom) = self.prom {
1023 prom.vm_destroyed_total.inc();
1024 prom.vm_count.with_label_values(&["ready"]).dec();
1025 }
1026
1027 self.event_emitter.emit(BoxEvent::empty("box.stopped"));
1029
1030 match stop_error {
1033 Some(e) => Err(e),
1034 None => Ok(()),
1035 }
1036 }
1037
1038 pub async fn set_busy(&self) -> Result<()> {
1040 let mut state = self.state.write().await;
1041
1042 if *state != BoxState::Ready {
1043 return Err(BoxError::StateError("VM not ready".to_string()));
1044 }
1045
1046 *state = BoxState::Busy;
1047 Ok(())
1048 }
1049
1050 pub async fn set_ready(&self) -> Result<()> {
1052 let mut state = self.state.write().await;
1053
1054 if *state != BoxState::Busy && *state != BoxState::Compacting {
1055 return Err(BoxError::StateError("Invalid state transition".to_string()));
1056 }
1057
1058 *state = BoxState::Ready;
1059 Ok(())
1060 }
1061
1062 pub async fn set_compacting(&self) -> Result<()> {
1064 let mut state = self.state.write().await;
1065
1066 if *state != BoxState::Busy {
1067 return Err(BoxError::StateError("VM not busy".to_string()));
1068 }
1069
1070 *state = BoxState::Compacting;
1071 Ok(())
1072 }
1073
1074 #[cfg(unix)]
1078 pub async fn pause(&self) -> Result<()> {
1079 let state = self.state.read().await;
1080 match *state {
1081 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
1082 BoxState::Created => {
1083 return Err(BoxError::StateError("VM not yet booted".to_string()));
1084 }
1085 BoxState::Stopped => {
1086 return Err(BoxError::StateError("VM is stopped".to_string()));
1087 }
1088 }
1089 drop(state);
1090
1091 if let Some(pid) = self.pid().await {
1092 let ret = unsafe { libc::kill(pid as i32, libc::SIGSTOP) };
1094 if ret != 0 {
1095 let err = std::io::Error::last_os_error();
1096 return Err(BoxError::ExecError(format!(
1097 "Failed to send SIGSTOP to pid {}: {}",
1098 pid, err
1099 )));
1100 }
1101 tracing::info!(box_id = %self.box_id, pid, "VM paused");
1102 Ok(())
1103 } else {
1104 Err(BoxError::StateError(
1105 "VM has no running process".to_string(),
1106 ))
1107 }
1108 }
1109
1110 #[cfg(unix)]
1114 pub async fn resume(&self) -> Result<()> {
1115 if let Some(pid) = self.pid().await {
1116 let ret = unsafe { libc::kill(pid as i32, libc::SIGCONT) };
1118 if ret != 0 {
1119 let err = std::io::Error::last_os_error();
1120 return Err(BoxError::ExecError(format!(
1121 "Failed to send SIGCONT to pid {}: {}",
1122 pid, err
1123 )));
1124 }
1125 tracing::info!(box_id = %self.box_id, pid, "VM resumed");
1126 Ok(())
1127 } else {
1128 Err(BoxError::StateError(
1129 "VM has no running process".to_string(),
1130 ))
1131 }
1132 }
1133
1134 #[cfg(windows)]
1136 pub async fn pause(&self) -> Result<()> {
1137 Err(BoxError::StateError(
1138 "VM pause is not yet supported on Windows".to_string(),
1139 ))
1140 }
1141
1142 #[cfg(windows)]
1144 pub async fn resume(&self) -> Result<()> {
1145 Err(BoxError::StateError(
1146 "VM resume is not yet supported on Windows".to_string(),
1147 ))
1148 }
1149
1150 pub async fn health_check(&self) -> Result<bool> {
1152 let state = self.state.read().await;
1153
1154 match *state {
1155 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {
1156 if let Some(ref handler) = *self.handler.read().await {
1158 Ok(handler.is_running())
1159 } else {
1160 Ok(false)
1161 }
1162 }
1163 _ => Ok(false),
1164 }
1165 }
1166
1167 pub async fn metrics(&self) -> Option<crate::vmm::VmMetrics> {
1169 let vm_metrics = self
1170 .handler
1171 .read()
1172 .await
1173 .as_ref()
1174 .map(|handler| handler.metrics())?;
1175
1176 if let Some(ref prom) = self.prom {
1178 prom.vm_cpu_percent
1179 .with_label_values(&[&self.box_id])
1180 .set(vm_metrics.cpu_percent.unwrap_or(0.0) as f64);
1181 prom.vm_memory_bytes
1182 .with_label_values(&[&self.box_id])
1183 .set(vm_metrics.memory_bytes.unwrap_or(0) as f64);
1184 }
1185
1186 Some(vm_metrics)
1187 }
1188
1189 pub async fn pid(&self) -> Option<u32> {
1191 self.handler
1192 .read()
1193 .await
1194 .as_ref()
1195 .map(|handler| handler.pid())
1196 }
1197
1198 #[cfg(unix)]
1200 pub fn tee(&self) -> Option<&dyn TeeExtension> {
1201 self.tee.as_deref()
1202 }
1203
1204 #[cfg(unix)]
1206 pub fn require_tee(&self) -> Result<&dyn TeeExtension> {
1207 self.tee.as_deref().ok_or_else(|| {
1208 BoxError::AttestationError("TEE is not configured for this box".to_string())
1209 })
1210 }
1211
1212 #[cfg(unix)]
1220 pub async fn update_resources(
1221 &self,
1222 update: &crate::resize::ResourceUpdate,
1223 ) -> Result<crate::resize::ResizeResult> {
1224 crate::resize::validate_update(update)?;
1226
1227 let mut result = crate::resize::ResizeResult {
1228 applied: Vec::new(),
1229 rejected: Vec::new(),
1230 };
1231
1232 if !update.has_tier2_changes() {
1233 return Ok(result);
1234 }
1235
1236 let commands = update.build_cgroup_commands();
1238 for cmd_str in &commands {
1239 let shell_cmd = vec!["sh".to_string(), "-c".to_string(), cmd_str.clone()];
1240
1241 match self.exec_command(shell_cmd, 5_000_000_000).await {
1242 Ok(output) if output.exit_code == 0 => {
1243 result.applied.push(cmd_str.clone());
1244 }
1245 Ok(output) => {
1246 let stderr = String::from_utf8_lossy(&output.stderr);
1247 let reason = if stderr.trim().is_empty() {
1248 format!("exit code {}", output.exit_code)
1249 } else {
1250 stderr.trim().to_string()
1251 };
1252 tracing::warn!(
1253 box_id = %self.box_id,
1254 cmd = %cmd_str,
1255 exit_code = output.exit_code,
1256 stderr = %stderr,
1257 "Cgroup update failed inside guest"
1258 );
1259 result.rejected.push((cmd_str.clone(), reason));
1260 }
1261 Err(e) => {
1262 tracing::warn!(
1263 box_id = %self.box_id,
1264 cmd = %cmd_str,
1265 error = %e,
1266 "Failed to exec cgroup update in guest"
1267 );
1268 result.rejected.push((cmd_str.clone(), e.to_string()));
1269 }
1270 }
1271 }
1272
1273 Ok(result)
1274 }
1275}
1276
1277#[cfg(unix)]
1282fn is_restore_mode(config: &BoxConfig) -> bool {
1283 config
1284 .restore_from
1285 .as_deref()
1286 .is_some_and(|s| !s.is_empty())
1287 || std::env::var("KRUN_RESTORE_FROM")
1288 .map(|v| !v.is_empty())
1289 .unwrap_or(false)
1290}
1291
1292pub(crate) fn fnv1a_hash(input: &str) -> u64 {
1294 let mut hash: u64 = 0xcbf29ce484222325;
1295 for byte in input.bytes() {
1296 hash ^= byte as u64;
1297 hash = hash.wrapping_mul(0x100000001b3);
1298 }
1299 hash
1300}
1301
1302#[cfg(unix)]
1303fn default_stop_signal() -> i32 {
1304 libc::SIGTERM
1305}
1306
1307#[cfg(windows)]
1308fn default_stop_signal() -> i32 {
1309 15
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314 use super::*;
1315 use a3s_box_core::event::EventEmitter;
1316 use std::sync::{
1317 atomic::{AtomicBool, Ordering},
1318 Arc,
1319 };
1320
1321 struct RecordingHandler {
1322 stopped: Arc<AtomicBool>,
1323 }
1324
1325 impl VmHandler for RecordingHandler {
1326 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1327 self.stopped.store(true, Ordering::SeqCst);
1328 Ok(())
1329 }
1330
1331 fn metrics(&self) -> crate::vmm::VmMetrics {
1332 crate::vmm::VmMetrics::default()
1333 }
1334
1335 fn is_running(&self) -> bool {
1336 true
1337 }
1338
1339 fn pid(&self) -> u32 {
1340 42
1341 }
1342 }
1343
1344 struct ExitStateHandler {
1345 exited: bool,
1346 }
1347
1348 impl VmHandler for ExitStateHandler {
1349 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1350 Ok(())
1351 }
1352
1353 fn metrics(&self) -> crate::vmm::VmMetrics {
1354 crate::vmm::VmMetrics::default()
1355 }
1356
1357 fn is_running(&self) -> bool {
1358 !self.exited
1359 }
1360
1361 fn has_exited(&self) -> bool {
1362 self.exited
1363 }
1364
1365 fn pid(&self) -> u32 {
1366 42
1367 }
1368 }
1369
1370 struct FailingHandler;
1372
1373 impl VmHandler for FailingHandler {
1374 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1375 Err(BoxError::StateError("simulated stop failure".to_string()))
1376 }
1377
1378 fn metrics(&self) -> crate::vmm::VmMetrics {
1379 crate::vmm::VmMetrics::default()
1380 }
1381
1382 fn is_running(&self) -> bool {
1383 true
1384 }
1385
1386 fn pid(&self) -> u32 {
1387 42
1388 }
1389 }
1390
1391 #[tokio::test]
1396 async fn test_destroy_runs_host_teardown_even_when_handler_stop_fails() {
1397 let tmp = tempfile::tempdir().unwrap();
1398 let box_id = "box-stopfail".to_string();
1399 let mut vm =
1401 VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
1402 vm.home_dir = tmp.path().to_path_buf();
1403 *vm.handler.write().await = Some(Box::new(FailingHandler));
1404
1405 let box_dir = tmp.path().join("boxes").join(&box_id);
1406 std::fs::create_dir_all(box_dir.join("logs")).unwrap();
1407
1408 let result = vm.destroy_with_options(default_stop_signal(), 100).await;
1409
1410 assert!(
1412 result.is_err(),
1413 "a handler-stop failure must still be reported"
1414 );
1415 assert!(vm.handler.read().await.is_none());
1417 assert!(
1418 !box_dir.exists(),
1419 "non-persistent box dir must be removed even when the stop failed"
1420 );
1421 }
1422
1423 #[tokio::test]
1424 async fn test_cleanup_boot_failure_stops_handler_and_removes_created_volumes() {
1425 let tmp = tempfile::tempdir().unwrap();
1426 let box_id = "box-test".to_string();
1427 let mut vm =
1428 VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
1429 vm.home_dir = tmp.path().to_path_buf();
1430 vm.anonymous_volumes = vec!["created-volume".to_string(), "reused-volume".to_string()];
1431 vm.created_anonymous_volumes = vec!["created-volume".to_string()];
1432
1433 let stopped = Arc::new(AtomicBool::new(false));
1434 *vm.handler.write().await = Some(Box::new(RecordingHandler {
1435 stopped: stopped.clone(),
1436 }));
1437
1438 let box_dir = tmp.path().join("boxes").join(&box_id);
1439 std::fs::create_dir_all(box_dir.join("logs")).unwrap();
1440
1441 let store = crate::volume::VolumeStore::new(
1442 tmp.path().join("volumes.json"),
1443 tmp.path().join("volumes"),
1444 );
1445 store
1446 .create(a3s_box_core::volume::VolumeConfig::new(
1447 "created-volume",
1448 "",
1449 ))
1450 .unwrap();
1451 store
1452 .create(a3s_box_core::volume::VolumeConfig::new("reused-volume", ""))
1453 .unwrap();
1454
1455 vm.cleanup_boot_failure().await;
1456
1457 assert!(stopped.load(Ordering::SeqCst));
1458 assert!(vm.handler.read().await.is_none());
1459 assert!(vm.created_anonymous_volumes.is_empty());
1460 assert_eq!(vm.anonymous_volumes, vec!["reused-volume".to_string()]);
1461 assert!(store.get("created-volume").unwrap().is_none());
1462 assert!(store.get("reused-volume").unwrap().is_some());
1463 assert!(!box_dir.exists());
1464 }
1465
1466 #[tokio::test]
1467 async fn test_wait_for_vm_running_returns_error_when_handler_exited() {
1468 let vm = VmManager::with_box_id(
1469 BoxConfig::default(),
1470 EventEmitter::new(16),
1471 "box-exited".to_string(),
1472 );
1473 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: true }));
1474
1475 let err = vm.wait_for_vm_running().await.unwrap_err();
1476
1477 assert!(err
1478 .to_string()
1479 .contains("VM process exited immediately after start"));
1480 }
1481
1482 #[tokio::test]
1483 async fn test_wait_for_vm_running_succeeds_when_handler_stays_running() {
1484 let config = BoxConfig {
1485 restore_from: Some("snapshot-path".to_string()),
1486 ..BoxConfig::default()
1487 };
1488 let vm = VmManager::with_box_id(config, EventEmitter::new(16), "box-running".to_string());
1489 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: false }));
1490
1491 vm.wait_for_vm_running().await.unwrap();
1492 }
1493
1494 #[tokio::test]
1495 async fn test_try_wait_exit_reads_guest_persisted_exit_code() {
1496 let tmp = tempfile::tempdir().unwrap();
1497 let box_id = "box-exit-code".to_string();
1498 let mut vm =
1499 VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
1500 vm.home_dir = tmp.path().to_path_buf();
1501
1502 let exit_path = tmp
1503 .path()
1504 .join("boxes")
1505 .join(&box_id)
1506 .join("upper")
1507 .join(".a3s_exit_code");
1508 std::fs::create_dir_all(exit_path.parent().unwrap()).unwrap();
1509 std::fs::write(&exit_path, "42\n").unwrap();
1510
1511 assert_eq!(vm.try_wait_exit().await.unwrap(), Some(42));
1512 assert_eq!(vm.exit_code(), Some(42));
1513 assert!(vm.has_exited().await);
1514 }
1515
1516 #[cfg(unix)]
1517 #[tokio::test]
1518 async fn test_wait_for_exec_ready_returns_when_handler_already_exited() {
1519 let mut vm = VmManager::with_box_id(
1520 BoxConfig::default(),
1521 EventEmitter::new(16),
1522 "box-exec-exited".to_string(),
1523 );
1524 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: true }));
1525 let tmp = tempfile::tempdir().unwrap();
1526
1527 vm.wait_for_exec_ready(&tmp.path().join("missing-exec.sock"))
1528 .await
1529 .unwrap();
1530
1531 assert!(vm.exec_client.is_none());
1532 }
1533
1534 #[cfg(unix)]
1535 #[tokio::test]
1536 async fn test_probe_exec_ready_once_ignores_missing_socket() {
1537 let mut vm = VmManager::with_box_id(
1538 BoxConfig::default(),
1539 EventEmitter::new(16),
1540 "box-probe".to_string(),
1541 );
1542 let tmp = tempfile::tempdir().unwrap();
1543
1544 vm.probe_exec_ready_once(&tmp.path().join("missing-exec.sock"))
1545 .await;
1546
1547 assert!(vm.exec_client.is_none());
1548 }
1549
1550 #[cfg(unix)]
1551 #[tokio::test]
1552 async fn test_attach_running_process_infers_port_forward_socket_path() {
1553 let mut vm = VmManager::with_box_id(
1554 BoxConfig::default(),
1555 EventEmitter::new(16),
1556 "box-test".to_string(),
1557 );
1558 let tmp = tempfile::tempdir().unwrap();
1559 let exec_socket_path = tmp.path().join("exec.sock");
1560 let pty_socket_path = Some(tmp.path().join("pty.sock"));
1561
1562 vm.attach_running_process(
1563 std::process::id(),
1564 exec_socket_path.clone(),
1565 pty_socket_path.clone(),
1566 )
1567 .await
1568 .unwrap();
1569
1570 assert_eq!(vm.exec_socket_path(), Some(exec_socket_path.as_path()));
1571 assert_eq!(vm.pty_socket_path(), pty_socket_path.as_deref());
1572 assert_eq!(
1573 vm.port_forward_socket_path(),
1574 Some(exec_socket_path.with_file_name("portfwd.sock").as_path())
1575 );
1576 assert_eq!(vm.state().await, BoxState::Ready);
1577 }
1578}