xbp 10.17.2

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::logging::{log_error, log_info, log_success};
use crate::utils::command_exists;
use std::fs;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::debug;

pub async fn install_api_service(port: u16, debug: bool) -> Result<(), String> {
    if !cfg!(target_os = "linux") {
        return Err("`xbp api install` is only supported on Linux/systemd hosts.".to_string());
    }
    if !command_exists("systemctl") {
        return Err(
            "`systemctl` was not found. Install systemd or manage the service manually."
                .to_string(),
        );
    }

    let exe = std::env::current_exe()
        .map_err(|e| format!("Failed to resolve current executable: {}", e))?;
    let exe_dir: PathBuf = exe
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from("."));

    let unit = format!(
        "[Unit]\nDescription=XBP API Server\nAfter=network.target\nWants=network-online.target\n\n[Service]\nType=simple\nEnvironment=XBP_API_BIND=127.0.0.1\nEnvironment=PORT_XBP_API={port}\nEnvironmentFile=-/etc/default/xbp\nWorkingDirectory={}\nExecStart={}\nRestart=always\nRestartSec=5\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=full\nProtectHome=read-only\nReadWritePaths=/var/log /etc/nginx /etc/systemd/system /run\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_OVERRIDE CAP_CHOWN CAP_SETUID CAP_SETGID\n\n[Install]\nWantedBy=multi-user.target\n",
        exe_dir.display(),
        exe.display()
    );

    let unit_path = "/etc/systemd/system/xbp-api.service";
    let temp_unit_path = "/tmp/xbp-api.service";
    let _ = log_info("api", "Writing systemd unit (requires sudo)", None).await;

    fs::write(temp_unit_path, unit).map_err(|e| {
        format!(
            "Failed to stage temporary unit file {}: {}",
            temp_unit_path, e
        )
    })?;

    let write_cmd = if should_use_sudo() {
        format!("sudo install -m 0644 {} {}", temp_unit_path, unit_path)
    } else {
        format!("install -m 0644 {} {}", temp_unit_path, unit_path)
    };
    if debug {
        debug!("Writing unit with: {}", write_cmd);
    }
    let write_output = Command::new("sh")
        .arg("-c")
        .arg(&write_cmd)
        .output()
        .await
        .map_err(|e| format!("Failed to write unit file: {}", e))?;
    if !write_output.status.success() {
        return Err(format!(
            "Writing unit failed: {}",
            String::from_utf8_lossy(&write_output.stderr)
        ));
    }
    let _ = fs::remove_file(temp_unit_path);

    run_systemctl(&["daemon-reload"], debug).await?;

    let enable_status = run_systemctl(&["enable", "--now", "xbp-api.service"], debug).await?;

    if !enable_status.success() {
        let _ = log_error(
            "api",
            "Failed to enable xbp-api.service",
            Some(&format!("{:?}", enable_status)),
        )
        .await;
        return Err("systemctl enable failed".into());
    }

    let _ = log_success(
        "api",
        &format!(
            "Installed and started xbp-api.service on PORT_XBP_API={}",
            port
        ),
        None,
    )
    .await;
    Ok(())
}

fn should_use_sudo() -> bool {
    is_root().map(|value| !value).unwrap_or(true)
}

fn is_root() -> Option<bool> {
    if !cfg!(target_os = "linux") {
        return None;
    }
    let uid = std::process::Command::new("id")
        .arg("-u")
        .output()
        .ok()
        .filter(|output| output.status.success())?;
    let raw = String::from_utf8_lossy(&uid.stdout);
    Some(raw.trim() == "0")
}

async fn run_systemctl(args: &[&str], debug: bool) -> Result<std::process::ExitStatus, String> {
    let mut command = if should_use_sudo() {
        if !command_exists("sudo") {
            return Err(
                "This action requires elevated privileges. Re-run as root or install `sudo`."
                    .to_string(),
            );
        }
        let mut cmd = Command::new("sudo");
        cmd.arg("systemctl");
        for arg in args {
            cmd.arg(arg);
        }
        cmd
    } else {
        let mut cmd = Command::new("systemctl");
        for arg in args {
            cmd.arg(arg);
        }
        cmd
    };

    if debug {
        debug!("Running systemctl {:?}", args);
    }

    command
        .status()
        .await
        .map_err(|e| format!("Failed to run systemctl {:?}: {}", args, e))
}