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 pub async fn try_wait_exit(&mut self) -> Result<Option<i32>> {
489 let mut handler = self.handler.write().await;
490 let Some(handler) = handler.as_mut() else {
491 return Ok(self.shim_exit_code);
492 };
493
494 if let Some(code) = handler.try_wait_exit()? {
495 self.shim_exit_code = Some(code);
496 return Ok(Some(code));
497 }
498
499 Ok(None)
500 }
501
502 #[cfg(unix)]
510 pub async fn run_deferred_main(
511 &mut self,
512 spec_json: &[u8],
513 timeout: std::time::Duration,
514 ) -> Result<a3s_box_core::exec::ExecOutput> {
515 let acked = {
516 let client = self
517 .exec_client
518 .as_ref()
519 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
520 client.spawn_main(Some(spec_json)).await?
521 };
522 if !acked {
523 return Err(BoxError::ExecError(
524 "spawn-main was not acknowledged by the guest".to_string(),
525 ));
526 }
527
528 let start = std::time::Instant::now();
530 let exit_code = loop {
531 if let Some(code) = self.try_wait_exit().await? {
532 break code;
533 }
534 if start.elapsed() >= timeout {
535 return Err(BoxError::ExecError(
536 "deferred main did not exit within the timeout".to_string(),
537 ));
538 }
539 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
540 };
541
542 let json_path = self
547 .home_dir
548 .join("boxes")
549 .join(&self.box_id)
550 .join("logs")
551 .join("container.json");
552 let drain_start = std::time::Instant::now();
553 let mut last_len = u64::MAX;
554 loop {
555 let len = std::fs::metadata(&json_path).map(|m| m.len()).unwrap_or(0);
556 if len == last_len || drain_start.elapsed() >= std::time::Duration::from_secs(1) {
557 break;
558 }
559 last_len = len;
560 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
561 }
562 let (stdout, stderr) = self.read_container_logs();
563 Ok(a3s_box_core::exec::ExecOutput {
564 stdout,
565 stderr,
566 exit_code,
567 })
568 }
569
570 fn read_container_logs(&self) -> (Vec<u8>, Vec<u8>) {
572 let path = self
573 .home_dir
574 .join("boxes")
575 .join(&self.box_id)
576 .join("logs")
577 .join("container.json");
578 let (mut out, mut err) = (Vec::new(), Vec::new());
579 if let Ok(content) = std::fs::read_to_string(&path) {
580 for line in content.lines() {
581 if let Ok(entry) = serde_json::from_str::<a3s_box_core::log::LogEntry>(line) {
582 if entry.stream == "stderr" {
583 err.extend_from_slice(entry.log.as_bytes());
584 } else {
585 out.extend_from_slice(entry.log.as_bytes());
586 }
587 }
588 }
589 }
590 (out, err)
591 }
592
593 #[cfg(unix)]
597 #[tracing::instrument(skip(self, request), fields(box_id = %self.box_id))]
598 pub async fn exec_request(
599 &self,
600 request: &a3s_box_core::exec::ExecRequest,
601 ) -> Result<a3s_box_core::exec::ExecOutput> {
602 if request.cmd.is_empty() {
603 return Err(BoxError::ExecError(
604 "Exec request requires a non-empty command".to_string(),
605 ));
606 }
607
608 let state = self.state.read().await;
609 match *state {
610 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
611 BoxState::Created => {
612 return Err(BoxError::ExecError("VM not yet booted".to_string()));
613 }
614 BoxState::Stopped => {
615 return Err(BoxError::ExecError("VM is stopped".to_string()));
616 }
617 }
618 drop(state);
619
620 let client = self
621 .exec_client
622 .as_ref()
623 .ok_or_else(|| BoxError::ExecError("Exec client not connected".to_string()))?;
624
625 let exec_start = std::time::Instant::now();
626 let result = client.exec_command(request).await;
627
628 if let Some(ref prom) = self.prom {
630 prom.exec_total.inc();
631 prom.exec_duration
632 .observe(exec_start.elapsed().as_secs_f64());
633 if result.is_err() || result.as_ref().is_ok_and(|o| o.exit_code != 0) {
634 prom.exec_errors_total.inc();
635 }
636 }
637
638 result
639 }
640
641 #[cfg(unix)]
645 #[tracing::instrument(skip(self, cmd), fields(box_id = %self.box_id))]
646 pub async fn exec_command(
647 &self,
648 cmd: Vec<String>,
649 timeout_ns: u64,
650 ) -> Result<a3s_box_core::exec::ExecOutput> {
651 let request = a3s_box_core::exec::ExecRequest {
652 cmd,
653 timeout_ns,
654 env: vec![],
655 working_dir: None,
656 rootfs: None,
657 stdin: None,
658 stdin_streaming: false,
659 user: None,
660 streaming: false,
661 };
662
663 self.exec_request(&request).await
664 }
665
666 pub async fn boot(&mut self) -> Result<()> {
668 let boot_span = tracing::info_span!("vm_boot", box_id = %self.box_id);
669 {
671 let state = self.state.read().await;
672 if *state != BoxState::Created {
673 return Err(BoxError::StateError("VM already booted".to_string()));
674 }
675 }
676
677 let boot_start = std::time::Instant::now();
678
679 tracing::info!(parent: &boot_span, box_id = %self.box_id, "Booting VM");
680
681 let layout = match self
683 .prepare_layout()
684 .instrument(tracing::info_span!(parent: &boot_span, "prepare_layout"))
685 .await
686 {
687 Ok(layout) => layout,
688 Err(error) => {
689 self.cleanup_boot_failure().await;
690 return Err(error);
691 }
692 };
693 self.image_config = layout.oci_config.clone();
694
695 let resolv_content = a3s_box_core::dns::generate_resolv_conf(&self.config.dns);
697 let resolv_path = layout.rootfs_path.join("etc/resolv.conf");
698 if let Err(e) = tokio::fs::write(&resolv_path, &resolv_content).await {
699 self.cleanup_boot_failure().await;
700 return Err(BoxError::IoError(e));
701 }
702 tracing::debug!(parent: &boot_span, dns = %resolv_content.trim(), "Configured guest DNS");
703
704 if let Err(e) = self.write_hostname_file(&layout) {
706 self.cleanup_boot_failure().await;
707 return Err(e);
708 }
709 if let Err(e) = self.write_standalone_hosts_file(&layout) {
710 self.cleanup_boot_failure().await;
711 return Err(e);
712 }
713
714 let mut spec = match self.build_instance_spec(&layout) {
716 Ok(s) => s,
717 Err(e) => {
718 self.cleanup_boot_failure().await;
719 return Err(e);
720 }
721 };
722
723 let bridge_network = match &self.config.network {
725 a3s_box_core::NetworkMode::Bridge { network } => Some(network.clone()),
726 _ => None,
727 };
728 if let Some(network_name) = bridge_network {
729 let net_config = match self.setup_bridge_network(&network_name) {
730 Ok(n) => n,
731 Err(e) => {
732 self.cleanup_boot_failure().await;
733 return Err(e);
734 }
735 };
736
737 match self.write_hosts_file(&layout, &network_name) {
739 Ok(()) => (),
740 Err(e) => {
741 self.cleanup_boot_failure().await;
742 return Err(e);
743 }
744 };
745
746 let ip_cidr = format!("{}/{}", net_config.ip_address, net_config.prefix_len);
749 spec.entrypoint
750 .env
751 .push(("A3S_NET_IP".to_string(), ip_cidr));
752 spec.entrypoint.env.push((
753 "A3S_NET_GATEWAY".to_string(),
754 net_config.gateway.to_string(),
755 ));
756 spec.entrypoint.env.push((
757 "A3S_NET_DNS".to_string(),
758 net_config
759 .dns_servers
760 .iter()
761 .map(|s| s.to_string())
762 .collect::<Vec<_>>()
763 .join(","),
764 ));
765
766 spec.network = Some(net_config);
767 }
768
769 if self.provider.is_none() {
771 let shim_path = match VmController::find_shim() {
772 Ok(p) => p,
773 Err(e) => {
774 self.cleanup_boot_failure().await;
775 return Err(e);
776 }
777 };
778 let controller = match VmController::new(shim_path) {
779 Ok(c) => c,
780 Err(e) => {
781 self.cleanup_boot_failure().await;
782 return Err(e);
783 }
784 };
785 self.provider = Some(Box::new(controller));
786 }
787
788 let handler = {
790 let provider = self
791 .provider
792 .as_ref()
793 .ok_or_else(|| BoxError::BoxBootError {
794 message: "VMM provider not initialized".to_string(),
795 hint: Some("Ensure VmManager has a provider set before boot".to_string()),
796 })?;
797 let vm_start_span = tracing::info_span!(parent: &boot_span, "vm_start");
798 match async { provider.start(&spec).await }
799 .instrument(vm_start_span)
800 .await
801 {
802 Ok(h) => h,
803 Err(e) => {
804 self.cleanup_boot_failure().await;
805 return Err(e);
806 }
807 }
808 };
809
810 *self.handler.write().await = Some(handler);
812
813 {
815 let wait_span = tracing::info_span!(parent: &boot_span, "wait_for_ready");
816 if let Err(e) = async {
817 self.wait_for_vm_running().await?;
818
819 #[cfg(unix)]
824 if is_restore_mode(&self.config) {
825 self.probe_exec_ready_once(&layout.exec_socket_path).await;
826 } else {
827 self.wait_for_exec_ready(&layout.exec_socket_path).await?;
828 }
829 Ok::<(), BoxError>(())
830 }
831 .instrument(wait_span)
832 .await
833 {
834 self.cleanup_boot_failure().await;
835 return Err(e);
836 }
837 }
838
839 #[cfg(unix)]
850 if !is_restore_mode(&self.config)
851 && std::env::var("BOX_DEFERRED_MAIN")
852 .map(|v| v == "1")
853 .unwrap_or(false)
854 {
855 if let Some(client) = self.exec_client.as_ref() {
856 match client.spawn_main(None).await {
857 Ok(true) => tracing::info!("deferred container main spawned"),
858 Ok(false) => tracing::warn!("deferred spawn-main not acknowledged"),
859 Err(e) => tracing::warn!(error = %e, "deferred spawn-main failed"),
860 }
861 }
862 }
863
864 self.exec_socket_path = Some(layout.exec_socket_path.clone());
866 self.pty_socket_path = Some(layout.pty_socket_path.clone());
867 self.port_forward_socket_path = Some(layout.port_forward_socket_path.clone());
868
869 #[cfg(unix)]
871 if !matches!(self.config.tee, TeeConfig::None) {
872 self.tee = Some(Box::new(crate::tee::SnpTeeExtension::new(
873 self.box_id.clone(),
874 layout.attest_socket_path.clone(),
875 )));
876 }
877
878 *self.state.write().await = BoxState::Ready;
880
881 if let Some(ref prom) = self.prom {
883 let boot_duration = boot_start.elapsed().as_secs_f64();
884 prom.vm_boot_duration.observe(boot_duration);
885 prom.vm_created_total.inc();
886 prom.vm_count.with_label_values(&["ready"]).inc();
887 }
888
889 self.event_emitter.emit(BoxEvent::empty("box.ready"));
891
892 tracing::info!(parent: &boot_span, box_id = %self.box_id, "VM ready");
893
894 Ok(())
895 }
896
897 pub async fn destroy(&mut self) -> Result<()> {
899 self.destroy_with_options(default_stop_signal(), DEFAULT_SHUTDOWN_TIMEOUT_MS)
900 .await
901 }
902
903 pub async fn destroy_with_timeout(&mut self, timeout_ms: u64) -> Result<()> {
905 self.destroy_with_options(libc::SIGTERM, timeout_ms).await
906 }
907
908 #[tracing::instrument(skip(self), fields(box_id = %self.box_id))]
913 pub async fn destroy_with_options(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
914 let mut state = self.state.write().await;
915
916 if *state == BoxState::Stopped {
917 return Ok(());
918 }
919
920 tracing::info!(box_id = %self.box_id, signal, timeout_ms, "Destroying VM");
921
922 *state = BoxState::Stopped;
924
925 let mut stop_error = None;
931 if let Some(mut handler) = self.handler.write().await.take() {
932 if let Err(e) = handler.stop(signal, timeout_ms) {
933 tracing::error!(box_id = %self.box_id, error = %e, "Failed to stop VM handler; continuing teardown");
934 stop_error = Some(e);
935 }
936 self.shim_exit_code = handler.exit_code();
937 }
938
939 if let Some(ref mut net) = self.net_manager {
941 net.stop();
942 }
943 self.net_manager = None;
944
945 let box_dir = self.home_dir.join("boxes").join(&self.box_id);
947 if let Err(e) = self
948 .rootfs_provider
949 .cleanup(&box_dir, self.config.persistent)
950 {
951 tracing::warn!(
952 box_id = %self.box_id,
953 error = %e,
954 "Failed to cleanup rootfs provider"
955 );
956 }
957
958 let socket_dir = self.socket_dir();
959 if let Err(e) = std::fs::remove_dir_all(&socket_dir) {
960 tracing::debug!(
961 box_id = %self.box_id,
962 path = %socket_dir.display(),
963 error = %e,
964 "Failed to cleanup VM socket directory"
965 );
966 }
967
968 if !self.config.persistent {
975 match std::fs::remove_dir_all(&box_dir) {
976 Ok(()) => {}
977 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
978 Err(e) => {
979 tracing::warn!(
980 box_id = %self.box_id,
981 path = %box_dir.display(),
982 error = %e,
983 "Failed to remove box directory on destroy"
984 );
985 }
986 }
987 }
988
989 if let Some(ref prom) = self.prom {
991 prom.vm_destroyed_total.inc();
992 prom.vm_count.with_label_values(&["ready"]).dec();
993 }
994
995 self.event_emitter.emit(BoxEvent::empty("box.stopped"));
997
998 match stop_error {
1001 Some(e) => Err(e),
1002 None => Ok(()),
1003 }
1004 }
1005
1006 pub async fn set_busy(&self) -> Result<()> {
1008 let mut state = self.state.write().await;
1009
1010 if *state != BoxState::Ready {
1011 return Err(BoxError::StateError("VM not ready".to_string()));
1012 }
1013
1014 *state = BoxState::Busy;
1015 Ok(())
1016 }
1017
1018 pub async fn set_ready(&self) -> Result<()> {
1020 let mut state = self.state.write().await;
1021
1022 if *state != BoxState::Busy && *state != BoxState::Compacting {
1023 return Err(BoxError::StateError("Invalid state transition".to_string()));
1024 }
1025
1026 *state = BoxState::Ready;
1027 Ok(())
1028 }
1029
1030 pub async fn set_compacting(&self) -> Result<()> {
1032 let mut state = self.state.write().await;
1033
1034 if *state != BoxState::Busy {
1035 return Err(BoxError::StateError("VM not busy".to_string()));
1036 }
1037
1038 *state = BoxState::Compacting;
1039 Ok(())
1040 }
1041
1042 #[cfg(unix)]
1046 pub async fn pause(&self) -> Result<()> {
1047 let state = self.state.read().await;
1048 match *state {
1049 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {}
1050 BoxState::Created => {
1051 return Err(BoxError::StateError("VM not yet booted".to_string()));
1052 }
1053 BoxState::Stopped => {
1054 return Err(BoxError::StateError("VM is stopped".to_string()));
1055 }
1056 }
1057 drop(state);
1058
1059 if let Some(pid) = self.pid().await {
1060 let ret = unsafe { libc::kill(pid as i32, libc::SIGSTOP) };
1062 if ret != 0 {
1063 let err = std::io::Error::last_os_error();
1064 return Err(BoxError::ExecError(format!(
1065 "Failed to send SIGSTOP to pid {}: {}",
1066 pid, err
1067 )));
1068 }
1069 tracing::info!(box_id = %self.box_id, pid, "VM paused");
1070 Ok(())
1071 } else {
1072 Err(BoxError::StateError(
1073 "VM has no running process".to_string(),
1074 ))
1075 }
1076 }
1077
1078 #[cfg(unix)]
1082 pub async fn resume(&self) -> Result<()> {
1083 if let Some(pid) = self.pid().await {
1084 let ret = unsafe { libc::kill(pid as i32, libc::SIGCONT) };
1086 if ret != 0 {
1087 let err = std::io::Error::last_os_error();
1088 return Err(BoxError::ExecError(format!(
1089 "Failed to send SIGCONT to pid {}: {}",
1090 pid, err
1091 )));
1092 }
1093 tracing::info!(box_id = %self.box_id, pid, "VM resumed");
1094 Ok(())
1095 } else {
1096 Err(BoxError::StateError(
1097 "VM has no running process".to_string(),
1098 ))
1099 }
1100 }
1101
1102 #[cfg(windows)]
1104 pub async fn pause(&self) -> Result<()> {
1105 Err(BoxError::StateError(
1106 "VM pause is not yet supported on Windows".to_string(),
1107 ))
1108 }
1109
1110 #[cfg(windows)]
1112 pub async fn resume(&self) -> Result<()> {
1113 Err(BoxError::StateError(
1114 "VM resume is not yet supported on Windows".to_string(),
1115 ))
1116 }
1117
1118 pub async fn health_check(&self) -> Result<bool> {
1120 let state = self.state.read().await;
1121
1122 match *state {
1123 BoxState::Ready | BoxState::Busy | BoxState::Compacting => {
1124 if let Some(ref handler) = *self.handler.read().await {
1126 Ok(handler.is_running())
1127 } else {
1128 Ok(false)
1129 }
1130 }
1131 _ => Ok(false),
1132 }
1133 }
1134
1135 pub async fn metrics(&self) -> Option<crate::vmm::VmMetrics> {
1137 let vm_metrics = self
1138 .handler
1139 .read()
1140 .await
1141 .as_ref()
1142 .map(|handler| handler.metrics())?;
1143
1144 if let Some(ref prom) = self.prom {
1146 prom.vm_cpu_percent
1147 .with_label_values(&[&self.box_id])
1148 .set(vm_metrics.cpu_percent.unwrap_or(0.0) as f64);
1149 prom.vm_memory_bytes
1150 .with_label_values(&[&self.box_id])
1151 .set(vm_metrics.memory_bytes.unwrap_or(0) as f64);
1152 }
1153
1154 Some(vm_metrics)
1155 }
1156
1157 pub async fn pid(&self) -> Option<u32> {
1159 self.handler
1160 .read()
1161 .await
1162 .as_ref()
1163 .map(|handler| handler.pid())
1164 }
1165
1166 #[cfg(unix)]
1168 pub fn tee(&self) -> Option<&dyn TeeExtension> {
1169 self.tee.as_deref()
1170 }
1171
1172 #[cfg(unix)]
1174 pub fn require_tee(&self) -> Result<&dyn TeeExtension> {
1175 self.tee.as_deref().ok_or_else(|| {
1176 BoxError::AttestationError("TEE is not configured for this box".to_string())
1177 })
1178 }
1179
1180 #[cfg(unix)]
1188 pub async fn update_resources(
1189 &self,
1190 update: &crate::resize::ResourceUpdate,
1191 ) -> Result<crate::resize::ResizeResult> {
1192 crate::resize::validate_update(update)?;
1194
1195 let mut result = crate::resize::ResizeResult {
1196 applied: Vec::new(),
1197 rejected: Vec::new(),
1198 };
1199
1200 if !update.has_tier2_changes() {
1201 return Ok(result);
1202 }
1203
1204 let commands = update.build_cgroup_commands();
1206 for cmd_str in &commands {
1207 let shell_cmd = vec!["sh".to_string(), "-c".to_string(), cmd_str.clone()];
1208
1209 match self.exec_command(shell_cmd, 5_000_000_000).await {
1210 Ok(output) if output.exit_code == 0 => {
1211 result.applied.push(cmd_str.clone());
1212 }
1213 Ok(output) => {
1214 let stderr = String::from_utf8_lossy(&output.stderr);
1215 let reason = if stderr.trim().is_empty() {
1216 format!("exit code {}", output.exit_code)
1217 } else {
1218 stderr.trim().to_string()
1219 };
1220 tracing::warn!(
1221 box_id = %self.box_id,
1222 cmd = %cmd_str,
1223 exit_code = output.exit_code,
1224 stderr = %stderr,
1225 "Cgroup update failed inside guest"
1226 );
1227 result.rejected.push((cmd_str.clone(), reason));
1228 }
1229 Err(e) => {
1230 tracing::warn!(
1231 box_id = %self.box_id,
1232 cmd = %cmd_str,
1233 error = %e,
1234 "Failed to exec cgroup update in guest"
1235 );
1236 result.rejected.push((cmd_str.clone(), e.to_string()));
1237 }
1238 }
1239 }
1240
1241 Ok(result)
1242 }
1243}
1244
1245#[cfg(unix)]
1250fn is_restore_mode(config: &BoxConfig) -> bool {
1251 config
1252 .restore_from
1253 .as_deref()
1254 .is_some_and(|s| !s.is_empty())
1255 || std::env::var("KRUN_RESTORE_FROM")
1256 .map(|v| !v.is_empty())
1257 .unwrap_or(false)
1258}
1259
1260pub(crate) fn fnv1a_hash(input: &str) -> u64 {
1262 let mut hash: u64 = 0xcbf29ce484222325;
1263 for byte in input.bytes() {
1264 hash ^= byte as u64;
1265 hash = hash.wrapping_mul(0x100000001b3);
1266 }
1267 hash
1268}
1269
1270#[cfg(unix)]
1271fn default_stop_signal() -> i32 {
1272 libc::SIGTERM
1273}
1274
1275#[cfg(windows)]
1276fn default_stop_signal() -> i32 {
1277 15
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283 use a3s_box_core::event::EventEmitter;
1284 use std::sync::{
1285 atomic::{AtomicBool, Ordering},
1286 Arc,
1287 };
1288
1289 struct RecordingHandler {
1290 stopped: Arc<AtomicBool>,
1291 }
1292
1293 impl VmHandler for RecordingHandler {
1294 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1295 self.stopped.store(true, Ordering::SeqCst);
1296 Ok(())
1297 }
1298
1299 fn metrics(&self) -> crate::vmm::VmMetrics {
1300 crate::vmm::VmMetrics::default()
1301 }
1302
1303 fn is_running(&self) -> bool {
1304 true
1305 }
1306
1307 fn pid(&self) -> u32 {
1308 42
1309 }
1310 }
1311
1312 struct ExitStateHandler {
1313 exited: bool,
1314 }
1315
1316 impl VmHandler for ExitStateHandler {
1317 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1318 Ok(())
1319 }
1320
1321 fn metrics(&self) -> crate::vmm::VmMetrics {
1322 crate::vmm::VmMetrics::default()
1323 }
1324
1325 fn is_running(&self) -> bool {
1326 !self.exited
1327 }
1328
1329 fn has_exited(&self) -> bool {
1330 self.exited
1331 }
1332
1333 fn pid(&self) -> u32 {
1334 42
1335 }
1336 }
1337
1338 struct FailingHandler;
1340
1341 impl VmHandler for FailingHandler {
1342 fn stop(&mut self, _signal: i32, _timeout_ms: u64) -> Result<()> {
1343 Err(BoxError::StateError("simulated stop failure".to_string()))
1344 }
1345
1346 fn metrics(&self) -> crate::vmm::VmMetrics {
1347 crate::vmm::VmMetrics::default()
1348 }
1349
1350 fn is_running(&self) -> bool {
1351 true
1352 }
1353
1354 fn pid(&self) -> u32 {
1355 42
1356 }
1357 }
1358
1359 #[tokio::test]
1364 async fn test_destroy_runs_host_teardown_even_when_handler_stop_fails() {
1365 let tmp = tempfile::tempdir().unwrap();
1366 let box_id = "box-stopfail".to_string();
1367 let mut vm =
1369 VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
1370 vm.home_dir = tmp.path().to_path_buf();
1371 *vm.handler.write().await = Some(Box::new(FailingHandler));
1372
1373 let box_dir = tmp.path().join("boxes").join(&box_id);
1374 std::fs::create_dir_all(box_dir.join("logs")).unwrap();
1375
1376 let result = vm.destroy_with_options(default_stop_signal(), 100).await;
1377
1378 assert!(
1380 result.is_err(),
1381 "a handler-stop failure must still be reported"
1382 );
1383 assert!(vm.handler.read().await.is_none());
1385 assert!(
1386 !box_dir.exists(),
1387 "non-persistent box dir must be removed even when the stop failed"
1388 );
1389 }
1390
1391 #[tokio::test]
1392 async fn test_cleanup_boot_failure_stops_handler_and_removes_created_volumes() {
1393 let tmp = tempfile::tempdir().unwrap();
1394 let box_id = "box-test".to_string();
1395 let mut vm =
1396 VmManager::with_box_id(BoxConfig::default(), EventEmitter::new(16), box_id.clone());
1397 vm.home_dir = tmp.path().to_path_buf();
1398 vm.anonymous_volumes = vec!["created-volume".to_string(), "reused-volume".to_string()];
1399 vm.created_anonymous_volumes = vec!["created-volume".to_string()];
1400
1401 let stopped = Arc::new(AtomicBool::new(false));
1402 *vm.handler.write().await = Some(Box::new(RecordingHandler {
1403 stopped: stopped.clone(),
1404 }));
1405
1406 let box_dir = tmp.path().join("boxes").join(&box_id);
1407 std::fs::create_dir_all(box_dir.join("logs")).unwrap();
1408
1409 let store = crate::volume::VolumeStore::new(
1410 tmp.path().join("volumes.json"),
1411 tmp.path().join("volumes"),
1412 );
1413 store
1414 .create(a3s_box_core::volume::VolumeConfig::new(
1415 "created-volume",
1416 "",
1417 ))
1418 .unwrap();
1419 store
1420 .create(a3s_box_core::volume::VolumeConfig::new("reused-volume", ""))
1421 .unwrap();
1422
1423 vm.cleanup_boot_failure().await;
1424
1425 assert!(stopped.load(Ordering::SeqCst));
1426 assert!(vm.handler.read().await.is_none());
1427 assert!(vm.created_anonymous_volumes.is_empty());
1428 assert_eq!(vm.anonymous_volumes, vec!["reused-volume".to_string()]);
1429 assert!(store.get("created-volume").unwrap().is_none());
1430 assert!(store.get("reused-volume").unwrap().is_some());
1431 assert!(!box_dir.exists());
1432 }
1433
1434 #[tokio::test]
1435 async fn test_wait_for_vm_running_returns_error_when_handler_exited() {
1436 let vm = VmManager::with_box_id(
1437 BoxConfig::default(),
1438 EventEmitter::new(16),
1439 "box-exited".to_string(),
1440 );
1441 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: true }));
1442
1443 let err = vm.wait_for_vm_running().await.unwrap_err();
1444
1445 assert!(err
1446 .to_string()
1447 .contains("VM process exited immediately after start"));
1448 }
1449
1450 #[tokio::test]
1451 async fn test_wait_for_vm_running_succeeds_when_handler_stays_running() {
1452 let config = BoxConfig {
1453 restore_from: Some("snapshot-path".to_string()),
1454 ..BoxConfig::default()
1455 };
1456 let vm = VmManager::with_box_id(config, EventEmitter::new(16), "box-running".to_string());
1457 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: false }));
1458
1459 vm.wait_for_vm_running().await.unwrap();
1460 }
1461
1462 #[cfg(unix)]
1463 #[tokio::test]
1464 async fn test_wait_for_exec_ready_returns_when_handler_already_exited() {
1465 let mut vm = VmManager::with_box_id(
1466 BoxConfig::default(),
1467 EventEmitter::new(16),
1468 "box-exec-exited".to_string(),
1469 );
1470 *vm.handler.write().await = Some(Box::new(ExitStateHandler { exited: true }));
1471 let tmp = tempfile::tempdir().unwrap();
1472
1473 vm.wait_for_exec_ready(&tmp.path().join("missing-exec.sock"))
1474 .await
1475 .unwrap();
1476
1477 assert!(vm.exec_client.is_none());
1478 }
1479
1480 #[cfg(unix)]
1481 #[tokio::test]
1482 async fn test_probe_exec_ready_once_ignores_missing_socket() {
1483 let mut vm = VmManager::with_box_id(
1484 BoxConfig::default(),
1485 EventEmitter::new(16),
1486 "box-probe".to_string(),
1487 );
1488 let tmp = tempfile::tempdir().unwrap();
1489
1490 vm.probe_exec_ready_once(&tmp.path().join("missing-exec.sock"))
1491 .await;
1492
1493 assert!(vm.exec_client.is_none());
1494 }
1495
1496 #[cfg(unix)]
1497 #[tokio::test]
1498 async fn test_attach_running_process_infers_port_forward_socket_path() {
1499 let mut vm = VmManager::with_box_id(
1500 BoxConfig::default(),
1501 EventEmitter::new(16),
1502 "box-test".to_string(),
1503 );
1504 let tmp = tempfile::tempdir().unwrap();
1505 let exec_socket_path = tmp.path().join("exec.sock");
1506 let pty_socket_path = Some(tmp.path().join("pty.sock"));
1507
1508 vm.attach_running_process(
1509 std::process::id(),
1510 exec_socket_path.clone(),
1511 pty_socket_path.clone(),
1512 )
1513 .await
1514 .unwrap();
1515
1516 assert_eq!(vm.exec_socket_path(), Some(exec_socket_path.as_path()));
1517 assert_eq!(vm.pty_socket_path(), pty_socket_path.as_deref());
1518 assert_eq!(
1519 vm.port_forward_socket_path(),
1520 Some(exec_socket_path.with_file_name("portfwd.sock").as_path())
1521 );
1522 assert_eq!(vm.state().await, BoxState::Ready);
1523 }
1524}