microsandbox_core/management/
orchestra.rs

1//! Orchestra management functionality for Microsandbox.
2//!
3//! This module provides functionality for managing collections of sandboxes in a coordinated way,
4//! similar to how container orchestration tools manage multiple containers. It handles the lifecycle
5//! of multiple sandboxes defined in configuration, including starting them up, shutting them down,
6//! and applying configuration changes.
7//!
8//! The main operations provided by this module are:
9//! - `up`: Start up all sandboxes defined in configuration
10//! - `down`: Gracefully shut down all running sandboxes
11//! - `apply`: Reconcile running sandboxes with configuration
12
13use 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
37//--------------------------------------------------------------------------------------------------
38// Constants
39//--------------------------------------------------------------------------------------------------
40
41/// TTL for cached directory sizes.
42const 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
53/// Global cache path -> (size, last_updated)
54static DISK_SIZE_CACHE: Lazy<RwLock<HashMap<String, (u64, Instant)>>> =
55    Lazy::new(|| RwLock::new(HashMap::new()));
56
57//--------------------------------------------------------------------------------------------------
58// Types
59//--------------------------------------------------------------------------------------------------
60
61/// Information about a sandbox's resource usage
62#[derive(Debug, Clone)]
63pub struct SandboxStatus {
64    /// The name of the sandbox
65    pub name: String,
66
67    /// Whether the sandbox is running
68    pub running: bool,
69
70    /// The PID of the supervisor process
71    pub supervisor_pid: Option<u32>,
72
73    /// The PID of the microVM process
74    pub microvm_pid: Option<u32>,
75
76    /// CPU usage percentage
77    pub cpu_usage: Option<f32>,
78
79    /// Memory usage in MiB
80    pub memory_usage: Option<u64>,
81
82    /// Disk usage of the RW layer in bytes
83    pub disk_usage: Option<u64>,
84
85    /// Rootfs paths
86    pub rootfs_paths: Option<String>,
87}
88
89//--------------------------------------------------------------------------------------------------
90// Functions
91//--------------------------------------------------------------------------------------------------
92
93/// Reconciles the running sandboxes with the configuration.
94///
95/// This function ensures that the set of running sandboxes matches what is defined in the
96/// configuration by:
97/// - Starting any sandboxes that are in the config but not running
98/// - Stopping any sandboxes that are running but not in the config
99///
100/// The function uses a file-based lock to prevent concurrent apply operations.
101/// If another apply operation is in progress, this function will fail immediately.
102/// The lock is automatically released when the function completes or if it fails.
103///
104/// ## Arguments
105///
106/// * `project_dir` - Optional path to the project directory. If None, defaults to current directory
107/// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename
108/// * `detach` - Whether to run sandboxes in detached mode (true) or with prefixed output (false)
109///
110/// ## Returns
111///
112/// Returns `MicrosandboxResult<()>` indicating success or failure. Possible failures include:
113/// - Config file not found or invalid
114/// - Database errors
115/// - Sandbox start/stop failures
116///
117/// ## Example
118///
119/// ```no_run
120/// use std::path::PathBuf;
121/// use microsandbox_core::management::orchestra;
122///
123/// #[tokio::main]
124/// async fn main() -> anyhow::Result<()> {
125///     // Apply configuration changes from the default microsandbox.yaml
126///     orchestra::apply(None, None, true).await?;
127///
128///     // Or specify a custom project directory and config file, in non-detached mode
129///     orchestra::apply(
130///         Some(&PathBuf::from("/path/to/project")),
131///         Some("custom-config.yaml"),
132///         false,
133///     ).await?;
134///     Ok(())
135/// }
136/// ```
137pub async fn apply(
138    project_dir: Option<&Path>,
139    config_file: Option<&str>,
140    detach: bool,
141) -> MicrosandboxResult<()> {
142    // Create spinner for CLI feedback
143    #[cfg(feature = "cli")]
144    let apply_config_sp = term::create_spinner(APPLY_CONFIG_MSG.to_string(), None, None);
145
146    // Load the configuration first to validate it exists before acquiring lock
147    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    // Ensure menv files exist
158    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    // Get database connection pool
166    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    // Get all sandboxes defined in config
177    let config_sandboxes = config.get_sandboxes();
178
179    // Get all running sandboxes from database
180    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    // Collect sandboxes that need to be started
192    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        // Start sandboxes in detached mode
201        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, // detached mode
210                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        // Start sandboxes in non-detached mode with multiplexed output
222        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            // Finish the spinner before running commands with output
240            #[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 early as we've already finished the spinner
248            return Ok(());
249        }
250    }
251
252    // Stop sandboxes that are active but not in config
253    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
273/// Starts specified sandboxes from the configuration if they are not already running.
274///
275/// This function ensures that the specified sandboxes are running by:
276/// - Starting any specified sandboxes that are in the config but not running
277/// - Ignoring sandboxes that are not specified or already running
278///
279/// ## Arguments
280///
281/// * `sandbox_names` - List of sandbox names to start
282/// * `project_dir` - Optional path to the project directory. If None, defaults to current directory
283/// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename
284/// * `detach` - Whether to run sandboxes in detached mode (true) or with prefixed output (false)
285///
286/// ## Returns
287///
288/// Returns `MicrosandboxResult<()>` indicating success or failure. Possible failures include:
289/// - Config file not found or invalid
290/// - Database errors
291/// - Sandbox start failures
292///
293/// ## Example
294///
295/// ```no_run
296/// use std::path::PathBuf;
297/// use microsandbox_core::management::orchestra;
298///
299/// #[tokio::main]
300/// async fn main() -> anyhow::Result<()> {
301///     // Start specific sandboxes from the default microsandbox.yaml in detached mode
302///     orchestra::up(vec!["sandbox1".to_string(), "sandbox2".to_string()], None, None, true).await?;
303///
304///     // Or specify a custom project directory and config file, in non-detached mode
305///     orchestra::up(
306///         vec!["sandbox1".to_string()],
307///         Some(&PathBuf::from("/path/to/project")),
308///         Some("custom-config.yaml"),
309///         false,
310///     ).await?;
311///     Ok(())
312/// }
313/// ```
314pub async fn up(
315    sandbox_names: Vec<String>,
316    project_dir: Option<&Path>,
317    config_file: Option<&str>,
318    detach: bool,
319) -> MicrosandboxResult<()> {
320    // Create spinner for CLI feedback
321    #[cfg(feature = "cli")]
322    let start_sandboxes_sp = term::create_spinner(START_SANDBOXES_MSG.to_string(), None, None);
323
324    // Load the configuration first to validate it exists
325    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    // Get all sandboxes defined in config
336    let config_sandboxes = config.get_sandboxes();
337
338    // Use all sandbox names from config if no names were specified
339    let sandbox_names_to_start = if sandbox_names.is_empty() {
340        // Use all sandbox names from config
341        config_sandboxes.keys().cloned().collect()
342    } else {
343        // Validate all sandbox names exist in config before proceeding
344        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    // Ensure menv files exist
359    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    // Get database connection pool
367    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    // Get all running sandboxes from database
378    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    // Collect sandboxes that need to be started
390    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        // Start specified sandboxes in detached mode
406        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, // detached mode
415                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        // Start sandboxes in non-detached mode with multiplexed output
427        let sandbox_commands = match prepare_sandbox_commands(
428            &sandboxes_to_start,
429            None, // Start script is None for normal up
430            &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            // Finish the spinner before running commands with output
445            #[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 early as we've already finished the spinner
453            return Ok(());
454        }
455    }
456
457    #[cfg(feature = "cli")]
458    start_sandboxes_sp.finish();
459
460    Ok(())
461}
462
463/// Stops specified sandboxes that are both in the configuration and currently running.
464///
465/// This function ensures that the specified sandboxes are stopped by:
466/// - Stopping any specified sandboxes that are both in the config and currently running
467/// - Ignoring sandboxes that are not specified, not in config, or not running
468///
469/// ## Arguments
470///
471/// * `sandbox_names` - List of sandbox names to stop
472/// * `project_dir` - Optional path to the project directory. If None, defaults to current directory
473/// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename
474///
475/// ## Returns
476///
477/// Returns `MicrosandboxResult<()>` indicating success or failure. Possible failures include:
478/// - Config file not found or invalid
479/// - Database errors
480/// - Sandbox stop failures
481///
482/// ## Example
483///
484/// ```no_run
485/// use std::path::PathBuf;
486/// use microsandbox_core::management::orchestra;
487///
488/// #[tokio::main]
489/// async fn main() -> anyhow::Result<()> {
490///     // Stop specific sandboxes from the default microsandbox.yaml
491///     orchestra::down(vec!["sandbox1".to_string(), "sandbox2".to_string()], None, None).await?;
492///
493///     // Or specify a custom project directory and config file
494///     orchestra::down(
495///         vec!["sandbox1".to_string()],
496///         Some(&PathBuf::from("/path/to/project")),
497///         Some("custom-config.yaml"),
498///     ).await?;
499///     Ok(())
500/// }
501/// ```
502pub async fn down(
503    sandbox_names: Vec<String>,
504    project_dir: Option<&Path>,
505    config_file: Option<&str>,
506) -> MicrosandboxResult<()> {
507    // Create spinner for CLI feedback
508    #[cfg(feature = "cli")]
509    let stop_sandboxes_sp = term::create_spinner(STOP_SANDBOXES_MSG.to_string(), None, None);
510
511    // Load the configuration first to validate it exists
512    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    // Get all sandboxes defined in config
523    let config_sandboxes = config.get_sandboxes();
524
525    // Use all sandbox names from config if no names were specified
526    let sandbox_names_to_stop = if sandbox_names.is_empty() {
527        // Use all sandbox names from config
528        config_sandboxes.keys().cloned().collect()
529    } else {
530        // Validate all sandbox names exist in config before proceeding
531        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    // Ensure menv files exist
546    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    // Get database connection pool
554    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    // Get all running sandboxes from database
565    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    // Stop specified sandboxes that are both in config and running
575    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
597/// Gets status information about specified sandboxes.
598///
599/// This function retrieves the current status and resource usage of the specified sandboxes:
600/// - Only reports on sandboxes that exist in the configuration
601/// - For each sandbox, reports whether it's running and resource usage if it is
602/// - If no sandbox names are specified (empty list), returns status for all sandboxes in the configuration
603///
604/// ## Arguments
605///
606/// * `sandbox_names` - List of sandbox names to get status for. If empty, all sandboxes in config are included.
607/// * `project_dir` - Optional path to the project directory. If None, defaults to current directory
608/// * `config_file` - Optional path to the Microsandbox config file. If None, uses default filename
609///
610/// ## Returns
611///
612/// Returns `MicrosandboxResult<Vec<SandboxStatus>>` containing status information for each sandbox.
613/// Possible failures include:
614/// - Config file not found or invalid
615/// - Database errors
616///
617/// ## Example
618///
619/// ```no_run
620/// use std::path::PathBuf;
621/// use microsandbox_core::management::orchestra;
622///
623/// #[tokio::main]
624/// async fn main() -> anyhow::Result<()> {
625///     // Get status of specific sandboxes from the default microsandbox.yaml
626///     let statuses = orchestra::status(
627///         vec!["sandbox1".to_string(), "sandbox2".to_string()],
628///         None,
629///         None
630///     ).await?;
631///
632///     // Or get status of all sandboxes from the default microsandbox.yaml
633///     let all_statuses = orchestra::status(
634///         vec![], // empty list means get all sandboxes
635///         None,
636///         None
637///     ).await?;
638///
639///     for status in statuses {
640///         println!("Sandbox: {}, Running: {}", status.name, status.running);
641///         if status.running {
642///             println!("  CPU: {:?}%, Memory: {:?}MiB, Disk: {:?}B",
643///                 status.cpu_usage, status.memory_usage, status.disk_usage);
644///         }
645///     }
646///
647///     Ok(())
648/// }
649/// ```
650pub async fn status(
651    sandbox_names: Vec<String>,
652    project_dir: Option<&Path>,
653    config_file: Option<&str>,
654) -> MicrosandboxResult<Vec<SandboxStatus>> {
655    // Load the configuration first to validate it exists
656    let (config, canonical_project_dir, config_file) =
657        config::load_config(project_dir, config_file).await?;
658
659    // Get all sandboxes defined in config
660    let config_sandboxes = config.get_sandboxes();
661
662    // Use all sandbox names from config if no names were specified
663    let sandbox_names_to_check = if sandbox_names.is_empty() {
664        // Use all sandbox names from config
665        config_sandboxes.keys().cloned().collect()
666    } else {
667        // Validate all sandbox names exist in config before proceeding
668        validate_sandbox_names(
669            &sandbox_names,
670            &config,
671            &canonical_project_dir,
672            &config_file,
673        )?;
674
675        sandbox_names
676    };
677
678    // Ensure menv files exist
679    let menv_path = canonical_project_dir.join(MICROSANDBOX_ENV_DIR);
680    menv::ensure_menv_files(&menv_path).await?;
681
682    // Get database connection pool
683    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    // Get all running sandboxes from database
687    let running_sandboxes = db::get_running_config_sandboxes(&pool, &config_file).await?;
688
689    // Create a HashMap for quick lookup of running sandboxes
690    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    // Get status for each sandbox name to check
697    let mut statuses = Vec::new();
698    for sandbox_name in &sandbox_names_to_check {
699        // Only process sandboxes that exist in config
700        if config_sandboxes.contains_key(sandbox_name) {
701            // Create a basic status with name and running status
702            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 the sandbox is running, get additional stats
714            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                    // Get CPU and memory usage for the microVM process
721                    if let Ok(mut process) = psutil::process::Process::new(sandbox.microvm_pid) {
722                        // CPU usage
723                        if let Ok(cpu_percent) = process.cpu_percent() {
724                            sandbox_status.cpu_usage = Some(cpu_percent);
725                        }
726
727                        // Memory usage
728                        if let Ok(memory_info) = process.memory_info() {
729                            // Convert bytes to MiB
730                            sandbox_status.memory_usage = Some(memory_info.rss() / (1024 * 1024));
731                        }
732                    }
733
734                    // Get disk usage of the RW layer if it's an overlayfs
735                    if sandbox.rootfs_paths.starts_with("overlayfs:") {
736                        let paths: Vec<&str> = sandbox.rootfs_paths.split(':').collect();
737                        if paths.len() > 1 {
738                            // The last path should be the RW layer
739                            let rw_path = paths.last().unwrap();
740                            if let Ok(metadata) = tokio::fs::metadata(rw_path).await {
741                                // For a directory, we need to calculate the total size
742                                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                        // For native rootfs, get the size of the rootfs
753                        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/// Show the status of the sandboxes
775///
776/// ## Arguments
777///
778/// * `names` - The names of the sandboxes to show the status of
779/// * `path` - The path to the microsandbox config file
780/// * `config` - The config file to use
781///
782/// ## Returns
783///
784/// Returns `MicrosandboxResult<()>` indicating success or failure. Possible failures include:
785/// - Config file not found or invalid
786/// - Database errors
787/// - Sandbox status retrieval failures
788///
789/// ## Example
790///
791/// ```no_run
792/// use std::path::PathBuf;
793/// use microsandbox_core::management::orchestra;
794///
795/// #[tokio::main]
796/// async fn main() -> anyhow::Result<()> {
797///     orchestra::show_status(
798///         &["sandbox1".to_string(), "sandbox2".to_string()],
799///         None,
800///         None
801///     ).await?;
802///     Ok(())
803/// }
804/// ```
805#[cfg(feature = "cli")]
806pub async fn show_status(
807    names: &[String],
808    path: Option<&Path>,
809    config: Option<&str>,
810) -> MicrosandboxResult<()> {
811    // Check if we're in a TTY to determine if we should do live updates
812    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        // Use a loop with tokio sleep for live updates
819        loop {
820            // Clear the screen by printing ANSI escape code
821            print!("\x1B[2J\x1B[1;1H");
822
823            display_status(&names, path.as_deref(), config.as_deref()).await?;
824
825            // Show update message
826            println!(
827                "\n{}",
828                style("Updating every 2 seconds. Press Ctrl+C to exit.").dim()
829            );
830
831            // Wait for the update interval
832            tokio::time::sleep(update_interval).await;
833        }
834    } else {
835        // Just display once for non-TTY
836        display_status(&names, path.as_deref(), config.as_deref()).await?;
837    }
838
839    Ok(())
840}
841
842/// Show status of sandboxes across multiple namespaces
843///
844/// This function displays the status of sandboxes from multiple namespaces in a consolidated view.
845/// It's useful for server mode when you want to see all sandboxes across all namespaces.
846///
847/// ## Arguments
848///
849/// * `names` - List of sandbox names to show status for. If empty, shows all sandboxes.
850/// * `namespaces_parent_dir` - The parent directory containing namespace directories
851///
852/// ## Returns
853///
854/// Returns `MicrosandboxResult<()>` indicating success or failure. Possible failures include:
855/// - Config file not found or invalid
856/// - Database errors
857/// - Sandbox status retrieval failures
858///
859/// ## Example
860///
861/// ```no_run
862/// use std::path::PathBuf;
863/// use microsandbox_core::management::orchestra;
864///
865/// #[tokio::main]
866/// async fn main() -> anyhow::Result<()> {
867///     // Get all namespace directories
868///     let namespaces = vec![
869///         PathBuf::from("/path/to/namespaces/ns1"),
870///         PathBuf::from("/path/to/namespaces/ns2"),
871///     ];
872///
873///     // Show status for all sandboxes in all namespaces
874///     orchestra::show_status_namespaces(&[], namespaces.iter().map(|p| p.as_path()).collect()).await?;
875///
876///     // Or show status for specific sandboxes
877///     orchestra::show_status_namespaces(
878///         &["sandbox1".to_string(), "sandbox2".to_string()],
879///         namespaces.iter().map(|p| p.as_path()).collect()
880///     ).await?;
881///
882///     Ok(())
883/// }
884/// ```
885#[cfg(feature = "cli")]
886pub async fn show_status_namespaces(
887    names: &[String],
888    namespaces_parent_dir: &Path,
889) -> MicrosandboxResult<()> {
890    // Check if we're in a TTY to determine if we should do live updates
891    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        // Use a loop with tokio sleep for live updates
898        loop {
899            // Clear the screen by printing ANSI escape code
900            print!("\x1B[2J\x1B[1;1H");
901
902            display_status_namespaces(names, namespaces_parent_dir).await?;
903
904            // Show update message
905            println!(
906                "\n{}",
907                style("Updating every 2 seconds. Press Ctrl+C to exit.").dim()
908            );
909
910            // Wait for the update interval
911            tokio::time::sleep(update_interval).await;
912        }
913    } else {
914        // Just display once for non-TTY
915        display_status_namespaces(names, namespaces_parent_dir).await?;
916    }
917
918    Ok(())
919}
920
921//--------------------------------------------------------------------------------------------------
922// Functions: Helpers
923//--------------------------------------------------------------------------------------------------
924
925// Helper function to prepare commands for multiple sandboxes
926async 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        // Don't print any individual sandbox preparation logs
936
937        let (command, _) = sandbox::prepare_run(
938            name,
939            script_name,
940            Some(project_dir),
941            Some(config_file),
942            vec![],
943            false, // non-detached
944            None,
945            true,
946        )
947        .await?;
948
949        commands.push((name.clone(), command));
950    }
951
952    Ok(commands)
953}
954
955// Helper function to run multiple commands with prefixed output
956async 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    // Exit early if no commands to run
965    if commands.is_empty() {
966        return Ok(());
967    }
968
969    // This will hold our child process handles and associated tasks
970    let mut children = Vec::new();
971    let mut output_tasks = Vec::new();
972
973    // Spawn all child processes
974    for (i, (sandbox_name, mut command)) in commands.into_iter().enumerate() {
975        // Configure command to pipe stdout and stderr
976        command.stdout(Stdio::piped());
977        command.stderr(Stdio::piped());
978
979        // Spawn the child process
980        let mut child = command.spawn()?;
981        let sandbox_name_clone = sandbox_name.clone();
982
983        // Style the sandbox name based on index
984        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        // Apply the same color to the separator bar
995        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        // Create task to handle stdout
1013        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                // Style the sandbox name and separator with color, but leave the message as plain text
1020                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                // Apply the same color to the separator bar
1031                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        // Create task to handle stderr
1046        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                // Style the sandbox name and separator with color, but leave the message as plain text
1052                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                // Apply the same color to the separator bar
1063                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        // Add to our collections
1078        children.push((sandbox_name, child));
1079        output_tasks.push(stdout_task);
1080        output_tasks.push(stderr_task);
1081    }
1082
1083    // Create task to monitor child processes
1084    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    // Wait for all processes to complete and output tasks to finish
1106    let statuses = monitor_task.await?;
1107    join_all(output_tasks).await;
1108
1109    // Check results and return error if any sandbox failed
1110    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        // Format failure message with colored sandbox names
1118        let error_msg = failed_sandboxes
1119            .iter()
1120            .enumerate()
1121            .map(|(i, (name, code))| {
1122                // Apply colors directly based on index
1123                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                // Apply the same color to the separator bar
1134                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// Extracted the status display logic to a separate function
1159#[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    // Sort the statuses in a stable order to prevent entries from moving around between updates
1168    // Order by: running status (running first), CPU usage (highest first),
1169    // memory usage (highest first), disk usage (highest first), and finally name (alphabetical)
1170    statuses.sort_by(|a, b| {
1171        // First compare by running status (running sandboxes first)
1172        let running_order = b.running.cmp(&a.running);
1173        if running_order != std::cmp::Ordering::Equal {
1174            return running_order;
1175        }
1176
1177        // Then compare by CPU usage (highest first)
1178        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        // Then compare by memory usage (highest first)
1187        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        // Then compare by disk usage (highest first)
1196        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        // Finally sort by name (alphabetical)
1205        a.name.cmp(&b.name)
1206    });
1207
1208    // Get current timestamp
1209    let now = chrono::Local::now();
1210    let timestamp = now.format("%Y-%m-%d %H:%M:%S");
1211
1212    // Display timestamp
1213    println!("{}", style(format!("Last updated: {}", timestamp)).dim());
1214
1215    // Print a table-like output with status information
1216    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// Update display_status_namespaces to discover namespaces dynamically
1246#[cfg(feature = "cli")]
1247async fn display_status_namespaces(
1248    names: &[String],
1249    namespaces_parent_dir: &Path,
1250) -> MicrosandboxResult<()> {
1251    // Create a struct to hold status with namespace info
1252    #[derive(Clone)]
1253    struct NamespacedStatus {
1254        namespace: String,
1255        status: SandboxStatus,
1256    }
1257
1258    // Collect statuses from all namespaces
1259    let mut all_statuses = Vec::new();
1260    let mut namespace_count = 0;
1261
1262    // Check if the parent directory exists
1263    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    // Scan the parent directory for namespaces
1271    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    // Sort namespace dirs alphabetically (initial sort to ensure deterministic behavior)
1282    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    // Process each namespace directory
1289    for namespace_dir in &namespace_dirs {
1290        // Extract namespace name from path
1291        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        // Get statuses for this namespace
1300        match status(names.to_vec(), Some(namespace_dir), None).await {
1301            Ok(statuses) => {
1302                // Add namespace info to each status
1303                for status in statuses {
1304                    all_statuses.push(NamespacedStatus {
1305                        namespace: namespace.clone(),
1306                        status,
1307                    });
1308                }
1309            }
1310            Err(e) => {
1311                // Log error but continue with other namespaces
1312                tracing::warn!("Error getting status for namespace {}: {}", namespace, e);
1313            }
1314        }
1315    }
1316
1317    // Group the statuses by namespace
1318    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    // Get current timestamp
1329    let now = chrono::Local::now();
1330    let timestamp = now.format("%Y-%m-%d %H:%M:%S");
1331
1332    // Display timestamp
1333    println!("{}", style(format!("Last updated: {}", timestamp)).dim());
1334
1335    // Prepare namespaces with their activity metrics for sorting
1336    #[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    // Calculate activity metrics for each namespace
1348    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    // Sort namespaces by activity level (running count first, then resource usage)
1367    namespace_activities.sort_by(|a, b| {
1368        // First by number of running sandboxes (descending)
1369        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        // Then by total CPU usage (descending)
1375        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        // Then by total memory usage (descending)
1384        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        // Finally by name (alphabetical) as a stable tiebreaker
1390        a.name.cmp(&b.name)
1391    });
1392
1393    // Capture sandboxes count
1394    let mut total_sandboxes = 0;
1395    let mut is_first = true;
1396
1397    // Display namespaces and their statuses with headers
1398    for activity in namespace_activities {
1399        // Add spacing between namespaces
1400        if !is_first {
1401            println!();
1402        }
1403        is_first = false;
1404
1405        // Print namespace header
1406        print_namespace_header(&activity.name);
1407
1408        // Sort the statuses in a stable order
1409        let mut statuses = activity.statuses;
1410        statuses.sort_by(|a, b| {
1411            // First compare by running status (running sandboxes first)
1412            let running_order = b.running.cmp(&a.running);
1413            if running_order != std::cmp::Ordering::Equal {
1414                return running_order;
1415            }
1416
1417            // Then compare by CPU usage (highest first)
1418            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            // Then compare by memory usage (highest first)
1427            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            // Then compare by disk usage (highest first)
1436            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            // Finally sort by name (alphabetical)
1445            a.name.cmp(&b.name)
1446        });
1447
1448        total_sandboxes += statuses.len();
1449
1450        // Print a table header for this namespace's sandboxes
1451        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        // Display the statuses for this namespace
1464        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    // Show summary with the captured counts
1480    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/// Prints a stylized header for namespace display
1492#[cfg(feature = "cli")]
1493fn print_namespace_header(namespace: &str) {
1494    // Create the simple title text without padding
1495    let title = format!("NAMESPACE: {}", namespace);
1496
1497    // Print the title with white color and underline styling
1498    println!("\n{}", style(title).white().bold());
1499
1500    // Print a separator line
1501    println!("{}", style("─".repeat(80)).dim());
1502}
1503
1504/// Formats the status columns for display
1505#[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
1560/// Validate that all requested sandbox names exist in the configuration
1561fn 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
1585/// Recursively calculate the size of a directory, but cache the result for a short period so that
1586/// callers (status refresh every ~2 s) don't hammer the filesystem.
1587async fn get_directory_size(path: &str) -> MicrosandboxResult<u64> {
1588    // First attempt to serve from cache
1589    {
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    // Need to (re)compute – perform blocking walk in a separate thread so we don't block Tokio
1599    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?; // propagates walkdir::Error (already covered in MicrosandboxError)
1606            if entry.file_type().is_file() {
1607                total += entry.metadata()?.len();
1608            }
1609        }
1610        Ok(total)
1611    })
1612    .await??; // first ? = JoinError, second ? = inner MicrosandboxError
1613
1614    // Update cache
1615    {
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
1623/// Checks if specified sandboxes from the configuration are running.
1624async 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    // Ensure menv files exist
1631    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    // Get database connection pool
1641    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    // Get all sandboxes defined in config
1645    let config_sandboxes = config.get_sandboxes();
1646
1647    // Get all running sandboxes from database
1648    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    // Check status of specified sandboxes
1653    let mut statuses = Vec::new();
1654    for sandbox_name in sandbox_names {
1655        // Only check if sandbox exists in config
1656        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}