xbp 10.26.3

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
#[cfg(feature = "systemd")]
use colored::Colorize;
#[cfg(feature = "systemd")]
use tokio::process::Command;

#[cfg(feature = "systemd")]
use crate::cli::ui;
#[cfg(feature = "systemd")]
use crate::commands::service::load_xbp_config;
#[cfg(feature = "systemd")]
use crate::strategies::{get_all_services, SystemdConfig, XbpConfig};
#[cfg(feature = "systemd")]
use crate::utils::command_exists;

#[cfg(feature = "systemd")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemdServiceState {
    Running,
    Stopped,
    Failed,
    Starting,
    Stopping,
    Unknown,
    NotFound,
    PermissionDenied,
}

#[cfg(feature = "systemd")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemdServiceStatus {
    pub name: String,
    pub state: SystemdServiceState,
    pub summary: String,
    pub detail: Option<String>,
}

#[cfg(feature = "systemd")]
pub struct SystemdRuntime {
    available: bool,
}

#[cfg(feature = "systemd")]
impl SystemdRuntime {
    pub fn detect() -> Self {
        Self {
            available: cfg!(target_os = "linux") && command_exists("systemctl"),
        }
    }

    pub fn unavailable() -> Self {
        Self { available: false }
    }

    pub async fn show_status(&self, debug: bool) -> Result<(), String> {
        let config = match load_xbp_config().await {
            Ok(cfg) => cfg,
            Err(_) => return Ok(()),
        };
        self.show_status_for_config(&config, debug).await
    }

    pub async fn show_status_for_config(
        &self,
        config: &XbpConfig,
        debug: bool,
    ) -> Result<(), String> {
        if !self.available {
            return Ok(());
        }

        let names = collect_configured_systemd_service_names(config);
        if names.is_empty() {
            return Ok(());
        }

        ui::section("Systemd Services");
        ui::divider(60);

        for name in names {
            let status = self.check_systemd_service_status(&name).await?;
            render_systemd_status(&status, debug);
        }

        Ok(())
    }

    async fn check_systemd_service_status(
        &self,
        service_name: &str,
    ) -> Result<SystemdServiceStatus, String> {
        let output = Command::new("systemctl")
            .arg("status")
            .arg(service_name)
            .arg("--no-pager")
            .output()
            .await
            .map_err(|e| format!("Failed to run systemctl: {}", e))?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        let (state, summary, detail) = classify_systemd_status(&stdout, &stderr);

        Ok(SystemdServiceStatus {
            name: service_name.to_string(),
            state,
            summary,
            detail,
        })
    }
}

#[cfg(feature = "systemd")]
pub async fn show_systemd_status(debug: bool) -> Result<(), String> {
    SystemdRuntime::detect().show_status(debug).await
}

#[cfg(feature = "systemd")]
pub fn collect_configured_systemd_service_names(config: &XbpConfig) -> Vec<String> {
    let mut services = Vec::new();

    if let Some(ref name) = config.systemd_service_name {
        push_deduped(&mut services, name.clone());
    }

    for service in get_all_services(config) {
        if let Some(ref name) = service.systemd_service_name {
            push_deduped(&mut services, name.clone());
        }
    }

    services
}

#[cfg(feature = "systemd")]
pub fn collect_systemd_runtime_settings(config: &XbpConfig) -> Option<SystemdConfig> {
    let project = config.systemd.clone();
    let service_configs = get_all_services(config);

    let mut combined = SystemdConfig::default();
    let mut any = false;

    if let Some(systemd) = project {
        append_runtime_settings(&mut combined, &systemd);
        any = true;
    }

    for service in service_configs {
        if let Some(systemd) = service.systemd {
            append_runtime_settings(&mut combined, &systemd);
            any = true;
        }
    }

    if any {
        Some(combined)
    } else {
        None
    }
}

#[cfg(feature = "systemd")]
fn append_runtime_settings(target: &mut SystemdConfig, source: &SystemdConfig) {
    merge_unique(&mut target.environment_files, &source.environment_files);
    merge_unique(&mut target.config_paths, &source.config_paths);
    merge_unique(&mut target.read_write_paths, &source.read_write_paths);
    merge_unique(&mut target.runtime_directories, &source.runtime_directories);
    merge_unique(&mut target.state_directories, &source.state_directories);
}

#[cfg(feature = "systemd")]
fn merge_unique(target: &mut Vec<String>, source: &[String]) {
    for value in source {
        push_deduped(target, value.clone());
    }
}

#[cfg(feature = "systemd")]
fn push_deduped(target: &mut Vec<String>, value: String) {
    if !target.iter().any(|existing| existing == &value) {
        target.push(value);
    }
}

#[cfg(feature = "systemd")]
fn classify_systemd_status(
    stdout: &str,
    stderr: &str,
) -> (SystemdServiceState, String, Option<String>) {
    let lower_stderr = stderr.to_lowercase();
    if lower_stderr.contains("could not be found") || lower_stderr.contains("not loaded") {
        return (
            SystemdServiceState::NotFound,
            "Not Found".to_string(),
            Some(stderr.trim().to_string()).filter(|s| !s.is_empty()),
        );
    }

    if lower_stderr.contains("permission denied")
        || lower_stderr.contains("failed to get properties")
    {
        return (
            SystemdServiceState::PermissionDenied,
            "Permission Denied".to_string(),
            Some(stderr.trim().to_string()).filter(|s| !s.is_empty()),
        );
    }

    let lower_stdout = stdout.to_lowercase();
    if lower_stdout.contains("active: active (running)") {
        (SystemdServiceState::Running, "Running".to_string(), None)
    } else if lower_stdout.contains("active: inactive (dead)") {
        (SystemdServiceState::Stopped, "Stopped".to_string(), None)
    } else if lower_stdout.contains("active: failed") {
        (
            SystemdServiceState::Failed,
            "Failed".to_string(),
            Some(stdout.lines().take(3).collect::<Vec<_>>().join("\n")).filter(|s| !s.is_empty()),
        )
    } else if lower_stdout.contains("active: activating") {
        (SystemdServiceState::Starting, "Starting".to_string(), None)
    } else if lower_stdout.contains("active: deactivating") {
        (SystemdServiceState::Stopping, "Stopping".to_string(), None)
    } else {
        (
            SystemdServiceState::Unknown,
            "Unknown".to_string(),
            Some(stdout.lines().take(3).collect::<Vec<_>>().join("\n")).filter(|s| !s.is_empty()),
        )
    }
}

#[cfg(feature = "systemd")]
fn render_systemd_status(status: &SystemdServiceStatus, debug: bool) {
    match status.state {
        SystemdServiceState::NotFound => {
            ui::status_line(&status.name, &status.summary, false);
            if debug {
                println!("    Service unit not found in systemd");
            }
            return;
        }
        SystemdServiceState::PermissionDenied => {
            ui::status_line(&status.name, &status.summary, false);
            if debug {
                println!("    Run with sudo to see full status");
            }
            return;
        }
        _ => {}
    }

    let ok = matches!(status.state, SystemdServiceState::Running);
    let state_label = match status.state {
        SystemdServiceState::Running => "Running".green(),
        SystemdServiceState::Stopped => "Stopped".dimmed(),
        SystemdServiceState::Failed => "Failed".red(),
        SystemdServiceState::Starting => "Starting".yellow(),
        SystemdServiceState::Stopping => "Stopping".yellow(),
        SystemdServiceState::Unknown => "Unknown".yellow(),
        SystemdServiceState::NotFound | SystemdServiceState::PermissionDenied => unreachable!(),
    };

    println!(
        "  {} {} {}",
        if ok {
            "".bright_green().bold()
        } else {
            "".bright_yellow().bold()
        },
        status.name.bright_white(),
        state_label
    );

    if debug {
        if let Some(detail) = &status.detail {
            for line in detail.lines().take(3) {
                println!("    {}", line.trim());
            }
        }
    }
}

#[cfg(feature = "systemd")]
#[cfg(test)]
mod tests {
    use super::*;
    use crate::strategies::{ServiceConfig, XbpConfig};

    fn base_config() -> XbpConfig {
        XbpConfig {
            project_name: "demo".to_string(),
            version: "0.1.0".to_string(),
            port: 3000,
            build_dir: "/tmp/demo".to_string(),
            app_type: None,
            build_command: None,
            start_command: None,
            install_command: None,
            environment: None,
            services: Some(vec![
                ServiceConfig {
                    name: "api".to_string(),
                    target: "rust".to_string(),
                    branch: "main".to_string(),
                    port: 8080,
                    root_directory: None,
                    environment: None,
                    url: None,
                    healthcheck_path: None,
                    restart_policy: None,
                    restart_policy_max_failure_count: None,
                    start_wrapper: None,
                    commands: None,
                    force_run_from_root: None,
                    systemd_service_name: Some("demo-api".to_string()),
                    systemd: None,
                },
                ServiceConfig {
                    name: "worker".to_string(),
                    target: "rust".to_string(),
                    branch: "main".to_string(),
                    port: 8081,
                    root_directory: None,
                    environment: None,
                    url: None,
                    healthcheck_path: None,
                    restart_policy: None,
                    restart_policy_max_failure_count: None,
                    start_wrapper: None,
                    commands: None,
                    force_run_from_root: None,
                    systemd_service_name: Some("demo-api".to_string()),
                    systemd: None,
                },
            ]),
            systemd_service_name: Some("demo".to_string()),
            systemd: Some(SystemdConfig {
                environment_files: vec!["/etc/default/demo".to_string()],
                config_paths: vec![],
                read_write_paths: vec![],
                runtime_directories: vec![],
                state_directories: vec![],
            }),
            kafka_brokers: None,
            kafka_topic: None,
            kafka_public_url: None,
            log_files: None,
            monitor_url: None,
            monitor_method: None,
            monitor_expected_code: None,
            monitor_interval: None,
            database: None,
            target: None,
            branch: None,
            crate_name: None,
            npm_script: None,
            port_storybook: None,
            url: None,
            url_storybook: None,
            linear: None,
            github: None,
        }
    }

    #[test]
    fn collects_service_names_in_config_order_without_duplicates() {
        let config = base_config();
        let names = collect_configured_systemd_service_names(&config);
        assert_eq!(names, vec!["demo".to_string(), "demo-api".to_string()]);
    }

    #[test]
    fn classify_not_found_status_from_stderr() {
        let (state, summary, _) =
            classify_systemd_status("", "Unit demo.service could not be found.");
        assert_eq!(state, SystemdServiceState::NotFound);
        assert_eq!(summary, "Not Found");
    }

    #[tokio::test]
    async fn no_configured_services_returns_ok() {
        let runtime = SystemdRuntime::detect();
        let mut config = base_config();
        config.systemd_service_name = None;
        config.services = Some(vec![]);
        runtime
            .show_status_for_config(&config, false)
            .await
            .expect("no-op");
    }

    #[tokio::test]
    async fn unavailable_runtime_short_circuits_without_error() {
        let runtime = SystemdRuntime::unavailable();
        let config = base_config();
        runtime
            .show_status_for_config(&config, false)
            .await
            .expect("short-circuit");
    }
}