use std::{
ffi::OsStr,
io,
process::{Command, Output, Stdio},
sync::{
Mutex, PoisonError,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use ic_host_process::{
child::CleanupPolicy,
signal::SignalActions,
tool::{
CompletedExecution, ExecutionFailure, OutputLimit, OutputLimits, ToolError,
capture_command, capture_group_command_with_cancellation,
},
};
use ic_host_text::escape::terminal_text;
use ic_host_tools::icp::{StatusOutput, decode_installation_status, status_command};
use icydb_schema::Principal;
pub(super) const PROBE_STDOUT_BYTES: usize = 16 * 1024 * 1024;
const PROBE_STDERR_BYTES: usize = 64 * 1024;
pub(super) fn probe_output(command: &mut Command) -> io::Result<Output> {
captured_output(capture_command(
command,
OutputLimits {
stdout: OutputLimit::Terminate(PROBE_STDOUT_BYTES),
stderr: OutputLimit::Terminate(PROBE_STDERR_BYTES),
timeout: Some(Duration::from_secs(30)),
},
))
}
pub(super) const UPDATE_OUTPUT_LIMITS: OutputLimits = OutputLimits {
stdout: OutputLimit::Terminate(PROBE_STDOUT_BYTES),
stderr: OutputLimit::Terminate(PROBE_STDERR_BYTES),
timeout: Some(Duration::from_mins(5)),
};
static UPDATE_SIGNALS: Mutex<()> = Mutex::new(());
static UPDATE_INTERRUPTED: AtomicBool = AtomicBool::new(false);
extern "C" fn update_interrupted(_: libc::c_int) {
UPDATE_INTERRUPTED.store(true, Ordering::Relaxed);
}
#[expect(
unsafe_code,
reason = "Host owns native actions; the CLI owns the exclusive async-signal-safe cancellation policy"
)]
pub(super) fn update_output(command: &mut Command) -> io::Result<Output> {
let _exclusive = UPDATE_SIGNALS
.lock()
.unwrap_or_else(PoisonError::into_inner);
UPDATE_INTERRUPTED.store(false, Ordering::Relaxed);
let _signals = unsafe {
SignalActions::install(
&[libc::SIGINT, libc::SIGTERM, libc::SIGHUP],
update_interrupted,
)
}
.map_err(io::Error::other)?;
captured_output(capture_group_command_with_cancellation(
command,
UPDATE_OUTPUT_LIMITS,
CleanupPolicy::KillAndWait,
None,
|| UPDATE_INTERRUPTED.load(Ordering::Relaxed),
))
}
pub(super) fn captured_output(result: Result<CompletedExecution, ToolError>) -> io::Result<Output> {
result
.map(|output| Output {
status: output.status,
stdout: output.stdout,
stderr: output.stderr,
})
.map_err(|error| {
let kind = match error.execution_error().map(|error| &error.failure) {
Some(ExecutionFailure::Io { source, .. }) => source.kind(),
_ => io::ErrorKind::Other,
};
io::Error::new(kind, error)
})
}
pub(super) fn capture_error_detail(error: &io::Error) -> String {
let source = error
.get_ref()
.and_then(|cause| cause.downcast_ref::<ToolError>())
.and_then(ToolError::execution_error)
.and_then(|execution| match &execution.failure {
ExecutionFailure::Io { source, .. } => Some(source),
_ => None,
});
let detail = source.map_or_else(|| error.to_string(), |source| format!("{error}: {source}"));
terminal_text(&detail).into_owned()
}
pub(super) fn run_external_command(mut command: Command, label: &str) -> Result<(), String> {
let status = command
.stdin(Stdio::null())
.status()
.map_err(|err| format!("{label}: {err}"))?;
if status.success() {
return Ok(());
}
Err(format!("{label} failed with {status}"))
}
pub(super) fn canister_is_installed(environment: &str, canister: &str) -> Result<bool, String> {
let output = canister_status_output(environment, canister, StatusOutput::Json)?;
decode_installation_status(&output.stdout)
.map_err(|error| terminal_text(&error.to_string()).into_owned())
}
pub(super) fn canister_id(environment: &str, canister: &str) -> Result<Option<String>, String> {
let output = canister_status_output(environment, canister, StatusOutput::IdOnly)?;
let id = std::str::from_utf8(&output.stdout)
.map_err(|error| format!("parse icp canister id UTF-8: {error}"))?
.trim();
if id.is_empty() {
return Ok(None);
}
Principal::from_text(id)
.map_err(|_| "icp canister status --id-only returned an invalid principal".to_string())?;
Ok(Some(id.to_owned()))
}
fn canister_status_output(
environment: &str,
canister: &str,
output: StatusOutput,
) -> Result<Output, String> {
let output = probe_output(&mut canister_status_command(environment, canister, output))
.map_err(|error| {
format!(
"failed to run icp canister status: {}",
capture_error_detail(&error)
)
})?;
if !output.status.success() {
return Err(format!(
"icp canister status for '{canister}' in environment '{environment}' failed with {}: {}",
output.status,
output_stderr(&output.stderr),
canister = terminal_text(canister),
environment = terminal_text(environment),
));
}
Ok(output)
}
pub(super) fn canister_status_command(
environment: &str,
canister: &str,
output: StatusOutput,
) -> Command {
let mut command = status_command(Command::new("icp"), OsStr::new(canister), output);
command
.arg("--environment")
.arg(environment)
.stdout(Stdio::piped());
command
}
pub(super) fn output_stderr(stderr: &[u8]) -> String {
ic_host_text::escape::terminal_text(String::from_utf8_lossy(stderr).trim()).into_owned()
}
#[cfg(test)]
mod tests;