1use crate::{
14 config::{Microsandbox, START_SCRIPT_NAME},
15 MicrosandboxError, MicrosandboxResult,
16};
17
18#[cfg(feature = "cli")]
19use console::style;
20#[cfg(feature = "cli")]
21use microsandbox_utils::term;
22use microsandbox_utils::{MICROSANDBOX_ENV_DIR, SANDBOX_DB_FILENAME};
23use nix::{
24 sys::signal::{self, Signal},
25 unistd::Pid,
26};
27use once_cell::sync::Lazy;
28use std::{
29 collections::HashMap,
30 path::{Path, PathBuf},
31 sync::RwLock,
32 time::{Duration, Instant},
33};
34
35use super::{config, db, menv, sandbox};
36
37const DISK_SIZE_TTL: Duration = Duration::from_secs(30);
43
44#[cfg(feature = "cli")]
45const APPLY_CONFIG_MSG: &str = "Applying sandbox configuration";
46
47#[cfg(feature = "cli")]
48const START_SANDBOXES_MSG: &str = "Starting sandboxes";
49
50#[cfg(feature = "cli")]
51const STOP_SANDBOXES_MSG: &str = "Stopping sandboxes";
52
53static DISK_SIZE_CACHE: Lazy<RwLock<HashMap<String, (u64, Instant)>>> =
55 Lazy::new(|| RwLock::new(HashMap::new()));
56
57#[derive(Debug, Clone)]
63pub struct SandboxStatus {
64 pub name: String,
66
67 pub running: bool,
69
70 pub supervisor_pid: Option<u32>,
72
73 pub microvm_pid: Option<u32>,
75
76 pub cpu_usage: Option<f32>,
78
79 pub memory_usage: Option<u64>,
81
82 pub disk_usage: Option<u64>,
84
85 pub rootfs_paths: Option<String>,
87}
88
89pub async fn apply(
138 project_dir: Option<&Path>,
139 config_file: Option<&str>,
140 detach: bool,
141) -> MicrosandboxResult<()> {
142 #[cfg(feature = "cli")]
144 let apply_config_sp = term::create_spinner(APPLY_CONFIG_MSG.to_string(), None, None);
145
146 let (config, canonical_project_dir, config_file) =
148 match config::load_config(project_dir, config_file).await {
149 Ok(result) => result,
150 Err(e) => {
151 #[cfg(feature = "cli")]
152 term::finish_with_error(&apply_config_sp);
153 return Err(e);
154 }
155 };
156
157 let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
159 if let Err(e) = menv::ensure_menv_files(&menv_path).await {
160 #[cfg(feature = "cli")]
161 term::finish_with_error(&apply_config_sp);
162 return Err(e);
163 }
164
165 let db_path = menv_path.join(SANDBOX_DB_FILENAME);
167 let pool = match db::get_or_create_pool(&db_path, &db::SANDBOX_DB_MIGRATOR).await {
168 Ok(pool) => pool,
169 Err(e) => {
170 #[cfg(feature = "cli")]
171 term::finish_with_error(&apply_config_sp);
172 return Err(e);
173 }
174 };
175
176 let config_sandboxes = config.get_sandboxes();
178
179 let running_sandboxes = match db::get_running_config_sandboxes(&pool, &config_file).await {
181 Ok(sandboxes) => sandboxes,
182 Err(e) => {
183 #[cfg(feature = "cli")]
184 term::finish_with_error(&apply_config_sp);
185 return Err(e);
186 }
187 };
188 let running_sandbox_names: Vec<String> =
189 running_sandboxes.iter().map(|s| s.name.clone()).collect();
190
191 let sandboxes_to_start: Vec<&String> = config_sandboxes
193 .keys()
194 .filter(|name| !running_sandbox_names.contains(*name))
195 .collect();
196
197 if sandboxes_to_start.is_empty() {
198 tracing::info!("No new sandboxes to start");
199 } else if detach {
200 for name in sandboxes_to_start {
202 tracing::info!("starting sandbox: {}", name);
203 if let Err(e) = sandbox::run(
204 name,
205 Some(START_SCRIPT_NAME),
206 Some(&canonical_project_dir),
207 Some(&config_file),
208 vec![],
209 true, None,
211 true,
212 )
213 .await
214 {
215 #[cfg(feature = "cli")]
216 term::finish_with_error(&apply_config_sp);
217 return Err(e);
218 }
219 }
220 } else {
221 let sandbox_commands = match prepare_sandbox_commands(
223 &sandboxes_to_start,
224 Some(START_SCRIPT_NAME),
225 &canonical_project_dir,
226 &config_file,
227 )
228 .await
229 {
230 Ok(commands) => commands,
231 Err(e) => {
232 #[cfg(feature = "cli")]
233 term::finish_with_error(&apply_config_sp);
234 return Err(e);
235 }
236 };
237
238 if !sandbox_commands.is_empty() {
239 #[cfg(feature = "cli")]
241 apply_config_sp.finish();
242
243 if let Err(e) = run_commands_with_prefixed_output(sandbox_commands).await {
244 return Err(e);
245 }
246
247 return Ok(());
249 }
250 }
251
252 for sandbox in running_sandboxes {
254 if !config_sandboxes.contains_key(&sandbox.name) {
255 tracing::info!("stopping sandbox: {}", sandbox.name);
256 if let Err(e) = signal::kill(
257 Pid::from_raw(sandbox.supervisor_pid as i32),
258 Signal::SIGTERM,
259 ) {
260 #[cfg(feature = "cli")]
261 term::finish_with_error(&apply_config_sp);
262 return Err(e.into());
263 }
264 }
265 }
266
267 #[cfg(feature = "cli")]
268 apply_config_sp.finish();
269
270 Ok(())
271}
272
273pub async fn up(
315 sandbox_names: Vec<String>,
316 project_dir: Option<&Path>,
317 config_file: Option<&str>,
318 detach: bool,
319) -> MicrosandboxResult<()> {
320 #[cfg(feature = "cli")]
322 let start_sandboxes_sp = term::create_spinner(START_SANDBOXES_MSG.to_string(), None, None);
323
324 let (config, canonical_project_dir, config_file) =
326 match config::load_config(project_dir, config_file).await {
327 Ok(result) => result,
328 Err(e) => {
329 #[cfg(feature = "cli")]
330 term::finish_with_error(&start_sandboxes_sp);
331 return Err(e);
332 }
333 };
334
335 let config_sandboxes = config.get_sandboxes();
337
338 let sandbox_names_to_start = if sandbox_names.is_empty() {
340 config_sandboxes.keys().cloned().collect()
342 } else {
343 if let Err(e) = validate_sandbox_names(
345 &sandbox_names,
346 &config,
347 &canonical_project_dir,
348 &config_file,
349 ) {
350 #[cfg(feature = "cli")]
351 term::finish_with_error(&start_sandboxes_sp);
352 return Err(e);
353 }
354
355 sandbox_names
356 };
357
358 let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
360 if let Err(e) = menv::ensure_menv_files(&menv_path).await {
361 #[cfg(feature = "cli")]
362 term::finish_with_error(&start_sandboxes_sp);
363 return Err(e);
364 }
365
366 let db_path = menv_path.join(SANDBOX_DB_FILENAME);
368 let pool = match db::get_or_create_pool(&db_path, &db::SANDBOX_DB_MIGRATOR).await {
369 Ok(pool) => pool,
370 Err(e) => {
371 #[cfg(feature = "cli")]
372 term::finish_with_error(&start_sandboxes_sp);
373 return Err(e);
374 }
375 };
376
377 let running_sandboxes = match db::get_running_config_sandboxes(&pool, &config_file).await {
379 Ok(sandboxes) => sandboxes,
380 Err(e) => {
381 #[cfg(feature = "cli")]
382 term::finish_with_error(&start_sandboxes_sp);
383 return Err(e);
384 }
385 };
386 let running_sandbox_names: Vec<String> =
387 running_sandboxes.iter().map(|s| s.name.clone()).collect();
388
389 let sandboxes_to_start: Vec<&String> = config_sandboxes
391 .keys()
392 .filter(|name| {
393 sandbox_names_to_start.contains(*name) && !running_sandbox_names.contains(*name)
394 })
395 .collect();
396
397 if sandboxes_to_start.is_empty() {
398 tracing::info!("No new sandboxes to start");
399 #[cfg(feature = "cli")]
400 start_sandboxes_sp.finish();
401 return Ok(());
402 }
403
404 if detach {
405 for name in sandboxes_to_start {
407 tracing::info!("starting sandbox: {}", name);
408 if let Err(e) = sandbox::run(
409 name,
410 None,
411 Some(&canonical_project_dir),
412 Some(&config_file),
413 vec![],
414 true, None,
416 true,
417 )
418 .await
419 {
420 #[cfg(feature = "cli")]
421 term::finish_with_error(&start_sandboxes_sp);
422 return Err(e);
423 }
424 }
425 } else {
426 let sandbox_commands = match prepare_sandbox_commands(
428 &sandboxes_to_start,
429 None, &canonical_project_dir,
431 &config_file,
432 )
433 .await
434 {
435 Ok(commands) => commands,
436 Err(e) => {
437 #[cfg(feature = "cli")]
438 term::finish_with_error(&start_sandboxes_sp);
439 return Err(e);
440 }
441 };
442
443 if !sandbox_commands.is_empty() {
444 #[cfg(feature = "cli")]
446 start_sandboxes_sp.finish();
447
448 if let Err(e) = run_commands_with_prefixed_output(sandbox_commands).await {
449 return Err(e);
450 }
451
452 return Ok(());
454 }
455 }
456
457 #[cfg(feature = "cli")]
458 start_sandboxes_sp.finish();
459
460 Ok(())
461}
462
463pub async fn down(
503 sandbox_names: Vec<String>,
504 project_dir: Option<&Path>,
505 config_file: Option<&str>,
506) -> MicrosandboxResult<()> {
507 #[cfg(feature = "cli")]
509 let stop_sandboxes_sp = term::create_spinner(STOP_SANDBOXES_MSG.to_string(), None, None);
510
511 let (config, canonical_project_dir, config_file) =
513 match config::load_config(project_dir, config_file).await {
514 Ok(result) => result,
515 Err(e) => {
516 #[cfg(feature = "cli")]
517 term::finish_with_error(&stop_sandboxes_sp);
518 return Err(e);
519 }
520 };
521
522 let config_sandboxes = config.get_sandboxes();
524
525 let sandbox_names_to_stop = if sandbox_names.is_empty() {
527 config_sandboxes.keys().cloned().collect()
529 } else {
530 if let Err(e) = validate_sandbox_names(
532 &sandbox_names,
533 &config,
534 &canonical_project_dir,
535 &config_file,
536 ) {
537 #[cfg(feature = "cli")]
538 term::finish_with_error(&stop_sandboxes_sp);
539 return Err(e);
540 }
541
542 sandbox_names
543 };
544
545 let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
547 if let Err(e) = menv::ensure_menv_files(&menv_path).await {
548 #[cfg(feature = "cli")]
549 term::finish_with_error(&stop_sandboxes_sp);
550 return Err(e);
551 }
552
553 let db_path = menv_path.join(SANDBOX_DB_FILENAME);
555 let pool = match db::get_or_create_pool(&db_path, &db::SANDBOX_DB_MIGRATOR).await {
556 Ok(pool) => pool,
557 Err(e) => {
558 #[cfg(feature = "cli")]
559 term::finish_with_error(&stop_sandboxes_sp);
560 return Err(e);
561 }
562 };
563
564 let running_sandboxes = match db::get_running_config_sandboxes(&pool, &config_file).await {
566 Ok(sandboxes) => sandboxes,
567 Err(e) => {
568 #[cfg(feature = "cli")]
569 term::finish_with_error(&stop_sandboxes_sp);
570 return Err(e);
571 }
572 };
573
574 for sandbox in running_sandboxes {
576 if sandbox_names_to_stop.contains(&sandbox.name)
577 && config_sandboxes.contains_key(&sandbox.name)
578 {
579 tracing::info!("stopping sandbox: {}", sandbox.name);
580 if let Err(e) = signal::kill(
581 Pid::from_raw(sandbox.supervisor_pid as i32),
582 Signal::SIGTERM,
583 ) {
584 #[cfg(feature = "cli")]
585 term::finish_with_error(&stop_sandboxes_sp);
586 return Err(e.into());
587 }
588 }
589 }
590
591 #[cfg(feature = "cli")]
592 stop_sandboxes_sp.finish();
593
594 Ok(())
595}
596
597pub async fn status(
651 sandbox_names: Vec<String>,
652 project_dir: Option<&Path>,
653 config_file: Option<&str>,
654) -> MicrosandboxResult<Vec<SandboxStatus>> {
655 let (config, canonical_project_dir, config_file) =
657 config::load_config(project_dir, config_file).await?;
658
659 let config_sandboxes = config.get_sandboxes();
661
662 let sandbox_names_to_check = if sandbox_names.is_empty() {
664 config_sandboxes.keys().cloned().collect()
666 } else {
667 validate_sandbox_names(
669 &sandbox_names,
670 &config,
671 &canonical_project_dir,
672 &config_file,
673 )?;
674
675 sandbox_names
676 };
677
678 let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
680 menv::ensure_menv_files(&menv_path).await?;
681
682 let db_path = menv_path.join(SANDBOX_DB_FILENAME);
684 let pool = db::get_or_create_pool(&db_path, &db::SANDBOX_DB_MIGRATOR).await?;
685
686 let running_sandboxes = db::get_running_config_sandboxes(&pool, &config_file).await?;
688
689 let running_sandbox_map: std::collections::HashMap<String, crate::models::Sandbox> =
691 running_sandboxes
692 .into_iter()
693 .map(|s| (s.name.clone(), s))
694 .collect();
695
696 let mut statuses = Vec::new();
698 for sandbox_name in &sandbox_names_to_check {
699 if config_sandboxes.contains_key(sandbox_name) {
701 let mut sandbox_status = SandboxStatus {
703 name: sandbox_name.clone(),
704 running: running_sandbox_map.contains_key(sandbox_name),
705 supervisor_pid: None,
706 microvm_pid: None,
707 cpu_usage: None,
708 memory_usage: None,
709 disk_usage: None,
710 rootfs_paths: None,
711 };
712
713 if sandbox_status.running {
715 if let Some(sandbox) = running_sandbox_map.get(sandbox_name) {
716 sandbox_status.supervisor_pid = Some(sandbox.supervisor_pid);
717 sandbox_status.microvm_pid = Some(sandbox.microvm_pid);
718 sandbox_status.rootfs_paths = Some(sandbox.rootfs_paths.clone());
719
720 if let Ok(mut process) = psutil::process::Process::new(sandbox.microvm_pid) {
722 if let Ok(cpu_percent) = process.cpu_percent() {
724 sandbox_status.cpu_usage = Some(cpu_percent);
725 }
726
727 if let Ok(memory_info) = process.memory_info() {
729 sandbox_status.memory_usage = Some(memory_info.rss() / (1024 * 1024));
731 }
732 }
733
734 if sandbox.rootfs_paths.starts_with("overlayfs:") {
736 let paths: Vec<&str> = sandbox.rootfs_paths.split(':').collect();
737 if paths.len() > 1 {
738 let rw_path = paths.last().unwrap();
740 if let Ok(metadata) = tokio::fs::metadata(rw_path).await {
741 if metadata.is_dir() {
743 if let Ok(size) = get_directory_size(rw_path).await {
744 sandbox_status.disk_usage = Some(size);
745 }
746 } else {
747 sandbox_status.disk_usage = Some(metadata.len());
748 }
749 }
750 }
751 } else if sandbox.rootfs_paths.starts_with("native:") {
752 let path = sandbox.rootfs_paths.strip_prefix("native:").unwrap();
754 if let Ok(metadata) = tokio::fs::metadata(path).await {
755 if metadata.is_dir() {
756 if let Ok(size) = get_directory_size(path).await {
757 sandbox_status.disk_usage = Some(size);
758 }
759 } else {
760 sandbox_status.disk_usage = Some(metadata.len());
761 }
762 }
763 }
764 }
765 }
766
767 statuses.push(sandbox_status);
768 }
769 }
770
771 Ok(statuses)
772}
773
774#[cfg(feature = "cli")]
806pub async fn show_status(
807 names: &[String],
808 path: Option<&Path>,
809 config: Option<&str>,
810) -> MicrosandboxResult<()> {
811 let is_tty = atty::is(atty::Stream::Stdout);
813 let live_view = is_tty;
814 let update_interval = std::time::Duration::from_secs(2);
815
816 if live_view {
817 println!("{}", style("Press Ctrl+C to exit live view").dim());
818 loop {
820 print!("\x1B[2J\x1B[1;1H");
822
823 display_status(&names, path.as_deref(), config.as_deref()).await?;
824
825 println!(
827 "\n{}",
828 style("Updating every 2 seconds. Press Ctrl+C to exit.").dim()
829 );
830
831 tokio::time::sleep(update_interval).await;
833 }
834 } else {
835 display_status(&names, path.as_deref(), config.as_deref()).await?;
837 }
838
839 Ok(())
840}
841
842#[cfg(feature = "cli")]
886pub async fn show_status_namespaces(
887 names: &[String],
888 namespaces_parent_dir: &Path,
889) -> MicrosandboxResult<()> {
890 let is_tty = atty::is(atty::Stream::Stdout);
892 let live_view = is_tty;
893 let update_interval = std::time::Duration::from_secs(2);
894
895 if live_view {
896 println!("{}", style("Press Ctrl+C to exit live view").dim());
897 loop {
899 print!("\x1B[2J\x1B[1;1H");
901
902 display_status_namespaces(names, namespaces_parent_dir).await?;
903
904 println!(
906 "\n{}",
907 style("Updating every 2 seconds. Press Ctrl+C to exit.").dim()
908 );
909
910 tokio::time::sleep(update_interval).await;
912 }
913 } else {
914 display_status_namespaces(names, namespaces_parent_dir).await?;
916 }
917
918 Ok(())
919}
920
921async fn prepare_sandbox_commands(
927 sandbox_names: &[&String],
928 script_name: Option<&str>,
929 project_dir: &Path,
930 config_file: &str,
931) -> MicrosandboxResult<Vec<(String, tokio::process::Command)>> {
932 let mut commands = Vec::new();
933
934 for &name in sandbox_names {
935 let (command, _) = sandbox::prepare_run(
938 name,
939 script_name,
940 Some(project_dir),
941 Some(config_file),
942 vec![],
943 false, None,
945 true,
946 )
947 .await?;
948
949 commands.push((name.clone(), command));
950 }
951
952 Ok(commands)
953}
954
955async fn run_commands_with_prefixed_output(
957 commands: Vec<(String, tokio::process::Command)>,
958) -> MicrosandboxResult<()> {
959 use console::style;
960 use futures::future::join_all;
961 use std::process::Stdio;
962 use tokio::io::{AsyncBufReadExt, BufReader};
963
964 if commands.is_empty() {
966 return Ok(());
967 }
968
969 let mut children = Vec::new();
971 let mut output_tasks = Vec::new();
972
973 for (i, (sandbox_name, mut command)) in commands.into_iter().enumerate() {
975 command.stdout(Stdio::piped());
977 command.stderr(Stdio::piped());
978
979 let mut child = command.spawn()?;
981 let sandbox_name_clone = sandbox_name.clone();
982
983 let styled_name = match i % 7 {
985 0 => style(&sandbox_name).green().bold(),
986 1 => style(&sandbox_name).blue().bold(),
987 2 => style(&sandbox_name).red().bold(),
988 3 => style(&sandbox_name).yellow().bold(),
989 4 => style(&sandbox_name).magenta().bold(),
990 5 => style(&sandbox_name).cyan().bold(),
991 _ => style(&sandbox_name).white().bold(),
992 };
993
994 let styled_separator = match i % 7 {
996 0 => style("|").green(),
997 1 => style("|").blue(),
998 2 => style("|").red(),
999 3 => style("|").yellow(),
1000 4 => style("|").magenta(),
1001 5 => style("|").cyan(),
1002 _ => style("|").white(),
1003 };
1004
1005 tracing::info!(
1006 "{} {} started supervisor process with PID: {}",
1007 styled_name,
1008 styled_separator,
1009 child.id().unwrap_or(0)
1010 );
1011
1012 let stdout = child.stdout.take().expect("Failed to capture stdout");
1014 let name_stdout = sandbox_name.clone();
1015 let color_index = i;
1016 let stdout_task = tokio::spawn(async move {
1017 let mut reader = BufReader::new(stdout).lines();
1018 while let Ok(Some(line)) = reader.next_line().await {
1019 let styled_name = match color_index % 7 {
1021 0 => style(&name_stdout).green().bold(),
1022 1 => style(&name_stdout).blue().bold(),
1023 2 => style(&name_stdout).red().bold(),
1024 3 => style(&name_stdout).yellow().bold(),
1025 4 => style(&name_stdout).magenta().bold(),
1026 5 => style(&name_stdout).cyan().bold(),
1027 _ => style(&name_stdout).white().bold(),
1028 };
1029
1030 let styled_separator = match color_index % 7 {
1032 0 => style("|").green(),
1033 1 => style("|").blue(),
1034 2 => style("|").red(),
1035 3 => style("|").yellow(),
1036 4 => style("|").magenta(),
1037 5 => style("|").cyan(),
1038 _ => style("|").white(),
1039 };
1040
1041 println!("{} {} {}", styled_name, styled_separator, line);
1042 }
1043 });
1044
1045 let stderr = child.stderr.take().expect("Failed to capture stderr");
1047 let color_index = i;
1048 let stderr_task = tokio::spawn(async move {
1049 let mut reader = BufReader::new(stderr).lines();
1050 while let Ok(Some(line)) = reader.next_line().await {
1051 let styled_name = match color_index % 7 {
1053 0 => style(&sandbox_name_clone).green().bold(),
1054 1 => style(&sandbox_name_clone).blue().bold(),
1055 2 => style(&sandbox_name_clone).red().bold(),
1056 3 => style(&sandbox_name_clone).yellow().bold(),
1057 4 => style(&sandbox_name_clone).magenta().bold(),
1058 5 => style(&sandbox_name_clone).cyan().bold(),
1059 _ => style(&sandbox_name_clone).white().bold(),
1060 };
1061
1062 let styled_separator = match color_index % 7 {
1064 0 => style("|").green(),
1065 1 => style("|").blue(),
1066 2 => style("|").red(),
1067 3 => style("|").yellow(),
1068 4 => style("|").magenta(),
1069 5 => style("|").cyan(),
1070 _ => style("|").white(),
1071 };
1072
1073 eprintln!("{} {} {}", styled_name, styled_separator, line);
1074 }
1075 });
1076
1077 children.push((sandbox_name, child));
1079 output_tasks.push(stdout_task);
1080 output_tasks.push(stderr_task);
1081 }
1082
1083 let monitor_task = tokio::spawn(async move {
1085 let mut statuses = Vec::new();
1086
1087 for (name, mut child) in children {
1088 match child.wait().await {
1089 Ok(status) => {
1090 let exit_code = status.code().unwrap_or(-1);
1091 let success = status.success();
1092 statuses.push((name, exit_code, success));
1093 }
1094 Err(_e) => {
1095 #[cfg(feature = "cli")]
1096 eprintln!("Error waiting for sandbox {}: {}", name, _e);
1097 statuses.push((name, -1, false));
1098 }
1099 }
1100 }
1101
1102 statuses
1103 });
1104
1105 let statuses = monitor_task.await?;
1107 join_all(output_tasks).await;
1108
1109 let failed_sandboxes: Vec<(String, i32)> = statuses
1111 .into_iter()
1112 .filter(|(_, _, success)| !success)
1113 .map(|(name, code, _)| (name, code))
1114 .collect();
1115
1116 if !failed_sandboxes.is_empty() {
1117 let error_msg = failed_sandboxes
1119 .iter()
1120 .enumerate()
1121 .map(|(i, (name, code))| {
1122 let styled_name = match i % 7 {
1124 0 => style(name).green().bold(),
1125 1 => style(name).blue().bold(),
1126 2 => style(name).red().bold(),
1127 3 => style(name).yellow().bold(),
1128 4 => style(name).magenta().bold(),
1129 5 => style(name).cyan().bold(),
1130 _ => style(name).white().bold(),
1131 };
1132
1133 let styled_separator = match i % 7 {
1135 0 => style("|").green(),
1136 1 => style("|").blue(),
1137 2 => style("|").red(),
1138 3 => style("|").yellow(),
1139 4 => style("|").magenta(),
1140 5 => style("|").cyan(),
1141 _ => style("|").white(),
1142 };
1143
1144 format!("{} {} exit code: {}", styled_name, styled_separator, code)
1145 })
1146 .collect::<Vec<_>>()
1147 .join(", ");
1148
1149 return Err(MicrosandboxError::SupervisorError(format!(
1150 "The following sandboxes failed: {}",
1151 error_msg
1152 )));
1153 }
1154
1155 Ok(())
1156}
1157
1158#[cfg(feature = "cli")]
1160async fn display_status(
1161 names: &[String],
1162 path: Option<&Path>,
1163 config: Option<&str>,
1164) -> MicrosandboxResult<()> {
1165 let mut statuses = status(names.to_vec(), path, config).await?;
1166
1167 statuses.sort_by(|a, b| {
1171 let running_order = b.running.cmp(&a.running);
1173 if running_order != std::cmp::Ordering::Equal {
1174 return running_order;
1175 }
1176
1177 let cpu_order = b
1179 .cpu_usage
1180 .partial_cmp(&a.cpu_usage)
1181 .unwrap_or(std::cmp::Ordering::Equal);
1182 if cpu_order != std::cmp::Ordering::Equal {
1183 return cpu_order;
1184 }
1185
1186 let memory_order = b
1188 .memory_usage
1189 .partial_cmp(&a.memory_usage)
1190 .unwrap_or(std::cmp::Ordering::Equal);
1191 if memory_order != std::cmp::Ordering::Equal {
1192 return memory_order;
1193 }
1194
1195 let disk_order = b
1197 .disk_usage
1198 .partial_cmp(&a.disk_usage)
1199 .unwrap_or(std::cmp::Ordering::Equal);
1200 if disk_order != std::cmp::Ordering::Equal {
1201 return disk_order;
1202 }
1203
1204 a.name.cmp(&b.name)
1206 });
1207
1208 let now = chrono::Local::now();
1210 let timestamp = now.format("%Y-%m-%d %H:%M:%S");
1211
1212 println!("{}", style(format!("Last updated: {}", timestamp)).dim());
1214
1215 println!(
1217 "\n{:<15} {:<10} {:<15} {:<12} {:<12} {:<12}",
1218 style("SANDBOX").bold(),
1219 style("STATUS").bold(),
1220 style("PIDS").bold(),
1221 style("CPU").bold(),
1222 style("MEMORY").bold(),
1223 style("DISK").bold()
1224 );
1225
1226 println!("{}", style("─".repeat(80)).dim());
1227
1228 for status in statuses {
1229 let (status_text, pids, cpu, memory, disk) = format_status_columns(&status);
1230
1231 println!(
1232 "{:<15} {:<10} {:<15} {:<12} {:<12} {:<12}",
1233 style(&status.name).bold(),
1234 status_text,
1235 pids,
1236 cpu,
1237 memory,
1238 disk
1239 );
1240 }
1241
1242 Ok(())
1243}
1244
1245#[cfg(feature = "cli")]
1247async fn display_status_namespaces(
1248 names: &[String],
1249 namespaces_parent_dir: &Path,
1250) -> MicrosandboxResult<()> {
1251 #[derive(Clone)]
1253 struct NamespacedStatus {
1254 namespace: String,
1255 status: SandboxStatus,
1256 }
1257
1258 let mut all_statuses = Vec::new();
1260 let mut namespace_count = 0;
1261
1262 if !namespaces_parent_dir.exists() {
1264 return Err(MicrosandboxError::PathNotFound(format!(
1265 "Namespaces directory not found at {}",
1266 namespaces_parent_dir.display()
1267 )));
1268 }
1269
1270 let mut entries = tokio::fs::read_dir(namespaces_parent_dir).await?;
1272 let mut namespace_dirs = Vec::new();
1273
1274 while let Some(entry) = entries.next_entry().await? {
1275 let path = entry.path();
1276 if path.is_dir() {
1277 namespace_dirs.push(path);
1278 }
1279 }
1280
1281 namespace_dirs.sort_by(|a, b| {
1283 let a_name = a.file_name().and_then(|n| n.to_str()).unwrap_or("");
1284 let b_name = b.file_name().and_then(|n| n.to_str()).unwrap_or("");
1285 a_name.cmp(b_name)
1286 });
1287
1288 for namespace_dir in &namespace_dirs {
1290 let namespace = namespace_dir
1292 .file_name()
1293 .and_then(|n| n.to_str())
1294 .unwrap_or("unknown")
1295 .to_string();
1296
1297 namespace_count += 1;
1298
1299 match status(names.to_vec(), Some(namespace_dir), None).await {
1301 Ok(statuses) => {
1302 for status in statuses {
1304 all_statuses.push(NamespacedStatus {
1305 namespace: namespace.clone(),
1306 status,
1307 });
1308 }
1309 }
1310 Err(e) => {
1311 tracing::warn!("Error getting status for namespace {}: {}", namespace, e);
1313 }
1314 }
1315 }
1316
1317 let mut statuses_by_namespace: std::collections::HashMap<String, Vec<SandboxStatus>> =
1319 std::collections::HashMap::new();
1320
1321 for namespaced_status in all_statuses {
1322 statuses_by_namespace
1323 .entry(namespaced_status.namespace)
1324 .or_default()
1325 .push(namespaced_status.status);
1326 }
1327
1328 let now = chrono::Local::now();
1330 let timestamp = now.format("%Y-%m-%d %H:%M:%S");
1331
1332 println!("{}", style(format!("Last updated: {}", timestamp)).dim());
1334
1335 #[derive(Clone)]
1337 struct NamespaceActivity {
1338 name: String,
1339 running_count: usize,
1340 total_cpu: f32,
1341 total_memory: u64,
1342 statuses: Vec<SandboxStatus>,
1343 }
1344
1345 let mut namespace_activities = Vec::new();
1346
1347 for (namespace, statuses) in statuses_by_namespace {
1349 if statuses.is_empty() {
1350 continue;
1351 }
1352
1353 let running_count = statuses.iter().filter(|s| s.running).count();
1354 let total_cpu: f32 = statuses.iter().filter_map(|s| s.cpu_usage).sum();
1355 let total_memory: u64 = statuses.iter().filter_map(|s| s.memory_usage).sum();
1356
1357 namespace_activities.push(NamespaceActivity {
1358 name: namespace,
1359 running_count,
1360 total_cpu,
1361 total_memory,
1362 statuses,
1363 });
1364 }
1365
1366 namespace_activities.sort_by(|a, b| {
1368 let running_order = b.running_count.cmp(&a.running_count);
1370 if running_order != std::cmp::Ordering::Equal {
1371 return running_order;
1372 }
1373
1374 let cpu_order = b
1376 .total_cpu
1377 .partial_cmp(&a.total_cpu)
1378 .unwrap_or(std::cmp::Ordering::Equal);
1379 if cpu_order != std::cmp::Ordering::Equal {
1380 return cpu_order;
1381 }
1382
1383 let memory_order = b.total_memory.cmp(&a.total_memory);
1385 if memory_order != std::cmp::Ordering::Equal {
1386 return memory_order;
1387 }
1388
1389 a.name.cmp(&b.name)
1391 });
1392
1393 let mut total_sandboxes = 0;
1395 let mut is_first = true;
1396
1397 for activity in namespace_activities {
1399 if !is_first {
1401 println!();
1402 }
1403 is_first = false;
1404
1405 print_namespace_header(&activity.name);
1407
1408 let mut statuses = activity.statuses;
1410 statuses.sort_by(|a, b| {
1411 let running_order = b.running.cmp(&a.running);
1413 if running_order != std::cmp::Ordering::Equal {
1414 return running_order;
1415 }
1416
1417 let cpu_order = b
1419 .cpu_usage
1420 .partial_cmp(&a.cpu_usage)
1421 .unwrap_or(std::cmp::Ordering::Equal);
1422 if cpu_order != std::cmp::Ordering::Equal {
1423 return cpu_order;
1424 }
1425
1426 let memory_order = b
1428 .memory_usage
1429 .partial_cmp(&a.memory_usage)
1430 .unwrap_or(std::cmp::Ordering::Equal);
1431 if memory_order != std::cmp::Ordering::Equal {
1432 return memory_order;
1433 }
1434
1435 let disk_order = b
1437 .disk_usage
1438 .partial_cmp(&a.disk_usage)
1439 .unwrap_or(std::cmp::Ordering::Equal);
1440 if disk_order != std::cmp::Ordering::Equal {
1441 return disk_order;
1442 }
1443
1444 a.name.cmp(&b.name)
1446 });
1447
1448 total_sandboxes += statuses.len();
1449
1450 println!(
1452 "{:<15} {:<10} {:<15} {:<12} {:<12} {:<12}",
1453 style("SANDBOX").bold(),
1454 style("STATUS").bold(),
1455 style("PIDS").bold(),
1456 style("CPU").bold(),
1457 style("MEMORY").bold(),
1458 style("DISK").bold()
1459 );
1460
1461 println!("{}", style("─".repeat(80)).dim());
1462
1463 for status in statuses {
1465 let (status_text, pids, cpu, memory, disk) = format_status_columns(&status);
1466
1467 println!(
1468 "{:<15} {:<10} {:<15} {:<12} {:<12} {:<12}",
1469 style(&status.name).bold(),
1470 status_text,
1471 pids,
1472 cpu,
1473 memory,
1474 disk
1475 );
1476 }
1477 }
1478
1479 println!(
1481 "\n{}: {}, {}: {}",
1482 style("Total Namespaces").dim(),
1483 namespace_count,
1484 style("Total Sandboxes").dim(),
1485 total_sandboxes
1486 );
1487
1488 Ok(())
1489}
1490
1491#[cfg(feature = "cli")]
1493fn print_namespace_header(namespace: &str) {
1494 let title = format!("NAMESPACE: {}", namespace);
1496
1497 println!("\n{}", style(title).white().bold());
1499
1500 println!("{}", style("─".repeat(80)).dim());
1502}
1503
1504#[cfg(feature = "cli")]
1506fn format_status_columns(
1507 status: &SandboxStatus,
1508) -> (
1509 console::StyledObject<String>,
1510 String,
1511 String,
1512 String,
1513 String,
1514) {
1515 let status_text = if status.running {
1516 style("RUNNING".to_string()).green()
1517 } else {
1518 style("STOPPED".to_string()).red()
1519 };
1520
1521 let pids = if status.running {
1522 format!(
1523 "{}/{}",
1524 status.supervisor_pid.unwrap_or(0),
1525 status.microvm_pid.unwrap_or(0)
1526 )
1527 } else {
1528 "-".to_string()
1529 };
1530
1531 let cpu = if let Some(cpu_usage) = status.cpu_usage {
1532 format!("{:.1}%", cpu_usage)
1533 } else {
1534 "-".to_string()
1535 };
1536
1537 let memory = if let Some(memory_usage) = status.memory_usage {
1538 format!("{} MiB", memory_usage)
1539 } else {
1540 "-".to_string()
1541 };
1542
1543 let disk = if let Some(disk_usage) = status.disk_usage {
1544 if disk_usage > 1024 * 1024 * 1024 {
1545 format!("{:.2} GB", disk_usage as f64 / (1024.0 * 1024.0 * 1024.0))
1546 } else if disk_usage > 1024 * 1024 {
1547 format!("{:.2} MB", disk_usage as f64 / (1024.0 * 1024.0))
1548 } else if disk_usage > 1024 {
1549 format!("{:.2} KB", disk_usage as f64 / 1024.0)
1550 } else {
1551 format!("{} B", disk_usage)
1552 }
1553 } else {
1554 "-".to_string()
1555 };
1556
1557 (status_text, pids, cpu, memory, disk)
1558}
1559
1560fn validate_sandbox_names(
1562 sandbox_names: &[String],
1563 config: &Microsandbox,
1564 project_dir: &Path,
1565 config_file: &str,
1566) -> MicrosandboxResult<()> {
1567 let config_sandboxes = config.get_sandboxes();
1568
1569 let missing_sandboxes: Vec<String> = sandbox_names
1570 .iter()
1571 .filter(|name| !config_sandboxes.contains_key(*name))
1572 .cloned()
1573 .collect();
1574
1575 if !missing_sandboxes.is_empty() {
1576 return Err(MicrosandboxError::SandboxNotFoundInConfig(
1577 missing_sandboxes.join(", "),
1578 project_dir.join(config_file),
1579 ));
1580 }
1581
1582 Ok(())
1583}
1584
1585async fn get_directory_size(path: &str) -> MicrosandboxResult<u64> {
1588 {
1590 let cache = DISK_SIZE_CACHE.read().unwrap();
1591 if let Some((size, ts)) = cache.get(path) {
1592 if ts.elapsed() < DISK_SIZE_TTL {
1593 return Ok(*size);
1594 }
1595 }
1596 }
1597
1598 let path_buf = PathBuf::from(path);
1600 let size = tokio::task::spawn_blocking(move || -> MicrosandboxResult<u64> {
1601 use walkdir::WalkDir;
1602
1603 let mut total: u64 = 0;
1604 for entry in WalkDir::new(&path_buf).follow_links(false) {
1605 let entry = entry?; if entry.file_type().is_file() {
1607 total += entry.metadata()?.len();
1608 }
1609 }
1610 Ok(total)
1611 })
1612 .await??; {
1616 let mut cache = DISK_SIZE_CACHE.write().unwrap();
1617 cache.insert(path.to_string(), (size, Instant::now()));
1618 }
1619
1620 Ok(size)
1621}
1622
1623async fn _check_running(
1625 sandbox_names: Vec<String>,
1626 config: &Microsandbox,
1627 project_dir: &Path,
1628 config_file: &str,
1629) -> MicrosandboxResult<Vec<(String, bool)>> {
1630 let canonical_project_dir = project_dir.canonicalize().map_err(|e| {
1632 MicrosandboxError::InvalidArgument(format!(
1633 "Failed to canonicalize project directory: {}",
1634 e
1635 ))
1636 })?;
1637 let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
1638 menv::ensure_menv_files(&menv_path).await?;
1639
1640 let db_path = menv_path.join(SANDBOX_DB_FILENAME);
1642 let pool = db::get_or_create_pool(&db_path, &db::SANDBOX_DB_MIGRATOR).await?;
1643
1644 let config_sandboxes = config.get_sandboxes();
1646
1647 let running_sandboxes = db::get_running_config_sandboxes(&pool, config_file).await?;
1649 let running_sandbox_names: Vec<String> =
1650 running_sandboxes.iter().map(|s| s.name.clone()).collect();
1651
1652 let mut statuses = Vec::new();
1654 for sandbox_name in sandbox_names {
1655 if config_sandboxes.contains_key(&sandbox_name) {
1657 let is_running = running_sandbox_names.contains(&sandbox_name);
1658 statuses.push((sandbox_name, is_running));
1659 }
1660 }
1661
1662 Ok(statuses)
1663}