canic-host 0.110.51

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
use std::{
    io::{self, Read, Write},
    path::Path,
    process::{Command, Stdio},
    thread,
};

use crate::{output_with_executable_busy_retry, spawn_with_executable_busy_retry};

use super::{
    command::{command_display, configure_inherited_fd, ensure_command_compatible},
    error::IcpCommandError,
    model::{IcpCli, IcpRawOutput},
    version::compatible_version_output,
};

/// Execute a command and capture trimmed stdout.
pub(super) fn run_output(command: &mut Command, icp: &IcpCli) -> Result<String, IcpCommandError> {
    icp.ensure_compatible_command(command)?;
    run_output_unchecked(command)
}

/// Execute a command whose successful stdout contains secret material.
///
/// The caller owns and must zero the returned allocation. This path neither
/// converts stdout into a `String` nor retains it in a diagnostic.
pub(super) fn run_secret_output(
    command: &mut Command,
    icp: &IcpCli,
) -> Result<Vec<u8>, IcpCommandError> {
    icp.ensure_compatible_command(command)?;
    let display = command_display(command);
    let mut output = output_with_executable_busy_retry(command)?;
    if output.status.success() {
        Ok(output.stdout)
    } else {
        output.stdout.fill(0);
        Err(IcpCommandError::Failed {
            command: display,
            stderr: command_stderr(&output),
        })
    }
}

fn run_output_unchecked(command: &mut Command) -> Result<String, IcpCommandError> {
    let display = command_display(command);
    let output = output_with_executable_busy_retry(command)?;
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        Err(IcpCommandError::Failed {
            command: display,
            stderr: command_stderr(&output),
        })
    }
}

/// Execute a command and capture stdout plus stderr on success.
pub fn run_output_with_stderr(command: &mut Command) -> Result<String, IcpCommandError> {
    ensure_command_compatible(command)?;
    let display = command_display(command);
    let output = output_with_executable_busy_retry(command)?;
    if output.status.success() {
        let mut text = String::from_utf8_lossy(&output.stdout).to_string();
        text.push_str(&String::from_utf8_lossy(&output.stderr));
        Ok(text.trim().to_string())
    } else {
        Err(IcpCommandError::Failed {
            command: display,
            stderr: command_stderr(&output),
        })
    }
}

/// Execute a command and parse successful stdout as JSON.
pub(super) fn run_json<T>(command: &mut Command, icp: &IcpCli) -> Result<T, IcpCommandError>
where
    T: serde::de::DeserializeOwned,
{
    icp.ensure_compatible_command(command)?;
    let display = command_display(command);
    let output = output_with_executable_busy_retry(command)?;
    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
        serde_json::from_str(&stdout).map_err(|source| IcpCommandError::Json {
            command: display,
            output: stdout,
            source,
        })
    } else {
        Err(IcpCommandError::Failed {
            command: display,
            stderr: command_stderr(&output),
        })
    }
}

/// Execute a command and require a successful status.
pub fn run_status(command: &mut Command) -> Result<(), IcpCommandError> {
    ensure_command_compatible(command)?;
    let display = command_display(command);
    let output = output_with_executable_busy_retry(command)?;
    if output.status.success() {
        Ok(())
    } else {
        Err(IcpCommandError::Failed {
            command: display,
            stderr: command_stderr(&output),
        })
    }
}

/// Execute a command with inherited terminal I/O and require a successful status.
pub(super) fn run_status_inherit(command: &mut Command) -> Result<(), IcpCommandError> {
    ensure_command_compatible(command)?;
    run_status_inherit_unchecked(command)
}

pub(super) fn run_status_inherit_unchecked(command: &mut Command) -> Result<(), IcpCommandError> {
    let display = command_display(command);
    command.stdout(Stdio::inherit()).stderr(Stdio::piped());
    let mut child = spawn_with_executable_busy_retry(command)?;
    let stderr_handle = child
        .stderr
        .take()
        .map(|stderr| thread::spawn(move || stream_and_capture_stderr(stderr)));
    let status = child.wait()?;
    let stderr = match stderr_handle {
        Some(handle) => match handle.join() {
            Ok(result) => result?,
            Err(_) => Vec::new(),
        },
        None => Vec::new(),
    };
    if status.success() {
        Ok(())
    } else {
        let stderr = if stderr.is_empty() {
            format!("command exited with status {}", exit_status_label(status))
        } else {
            String::from_utf8_lossy(&stderr).to_string()
        };
        Err(IcpCommandError::Failed {
            command: display,
            stderr,
        })
    }
}

fn stream_and_capture_stderr(mut stderr: impl Read) -> io::Result<Vec<u8>> {
    let mut captured = Vec::new();
    let mut buffer = [0_u8; 8192];
    let mut terminal = io::stderr().lock();
    loop {
        let read = stderr.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        terminal.write_all(&buffer[..read])?;
        captured.extend_from_slice(&buffer[..read]);
    }
    terminal.flush()?;
    Ok(captured)
}

/// Execute a command and return whether it exits successfully.
pub fn run_success(command: &mut Command) -> Result<bool, IcpCommandError> {
    ensure_command_compatible(command)?;
    Ok(output_with_executable_busy_retry(command)?.status.success())
}

/// Execute a rendered ICP CLI command and return raw process output.
pub fn run_raw_output(
    program: &str,
    args: &[String],
    inherited_fd: Option<i32>,
) -> Result<IcpRawOutput, std::io::Error> {
    if is_icp_program(program) {
        compatible_version_output(program, None)
            .map_err(|err| io::Error::other(err.to_string()))?;
    }
    let mut command = Command::new(program);
    command.args(args);
    configure_inherited_fd(&mut command, inherited_fd);
    let output = output_with_executable_busy_retry(&mut command)?;
    Ok(IcpRawOutput {
        success: output.status.success(),
        status: exit_status_label(output.status),
        stdout: output.stdout,
        stderr: output.stderr,
    })
}

fn is_icp_program(program: &str) -> bool {
    Path::new(program)
        .file_name()
        .is_some_and(|file_name| file_name == "icp")
}

// Prefer stderr, but keep stdout diagnostics for CLI commands that report there.
pub(super) fn command_stderr(output: &std::process::Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr);
    if stderr.trim().is_empty() {
        String::from_utf8_lossy(&output.stdout).to_string()
    } else {
        stderr.to_string()
    }
}

// Render process exit status without relying on platform-specific internals.
fn exit_status_label(status: std::process::ExitStatus) -> String {
    status
        .code()
        .map_or_else(|| "signal".to_string(), |code| code.to_string())
}
// Operation-local successful qualification is informational, never controller authority.
impl IcpCli {
    pub(super) fn ensure_compatible_command(
        &self,
        command: &Command,
    ) -> Result<(), IcpCommandError> {
        if command.get_program() == std::ffi::OsStr::new(&self.executable)
            && command.get_current_dir() == self.cwd.as_deref()
        {
            self.compatible_version().map(|_| ())
        } else {
            ensure_command_compatible(command)
        }
    }
}