use std::ffi::OsString;
use std::process::{Command, ExitStatus, Stdio};
use ready_set_sdk::{CapabilityReport, CapabilityRunReport, ExitCode, ProviderId};
use crate::discovery::find_plugin;
use crate::env::{EnvContract, export_contract};
#[derive(Debug)]
pub enum ReadyInvocation {
Report(CapabilityReport),
ProviderUnavailable {
summary: String,
},
ProviderFailed {
summary: String,
},
}
#[derive(Debug)]
pub enum SetInvocation {
Report(CapabilityRunReport),
Streamed {
exit_code: ExitCode,
},
ProviderUnavailable {
summary: String,
},
ProviderFailed {
exit_code: ExitCode,
summary: String,
},
}
#[derive(Debug)]
pub enum GoInvocation {
Report {
report: CapabilityRunReport,
exit_code: ExitCode,
},
Streamed {
exit_code: ExitCode,
},
ProviderUnavailable {
summary: String,
},
ProviderFailed {
exit_code: ExitCode,
summary: String,
},
}
pub fn invoke_ready(
provider: &ProviderId,
capability: &str,
contract: &EnvContract,
) -> std::io::Result<ReadyInvocation> {
let Some(entry) = find_plugin(provider.as_str()) else {
return Ok(ReadyInvocation::ProviderUnavailable {
summary: format!("provider `{provider}` is not installed"),
});
};
let output = command_for_provider(&entry.binary_path, contract)
.arg("__ready")
.arg(capability)
.env("READY_SET_OUTPUT", "json")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
if !output.status.success() {
return Ok(ReadyInvocation::ProviderFailed {
summary: provider_failure_summary(provider, "__ready", &output.stderr),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<CapabilityReport>(stdout.trim()) {
Ok(report) => Ok(ReadyInvocation::Report(report)),
Err(err) => Ok(ReadyInvocation::ProviderFailed {
summary: format!("provider `{provider}` returned invalid readiness JSON: {err}"),
}),
}
}
pub fn invoke_set(
provider: &ProviderId,
capability: &str,
args: &[OsString],
contract: &EnvContract,
capture_json: bool,
) -> std::io::Result<SetInvocation> {
let Some(entry) = find_plugin(provider.as_str()) else {
return Ok(SetInvocation::ProviderUnavailable {
summary: format!("provider `{provider}` is not installed"),
});
};
let mut cmd = command_for_provider(&entry.binary_path, contract);
cmd.arg("__set").arg(capability).args(args);
if !capture_json {
let status = cmd.status()?;
return Ok(SetInvocation::Streamed {
exit_code: exit_code_from_status(status),
});
}
let output = cmd
.env("READY_SET_OUTPUT", "json")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
let exit_code = exit_code_from_status(output.status);
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<CapabilityRunReport>(stdout.trim()) {
Ok(report) if output.status.success() => Ok(SetInvocation::Report(report)),
Ok(_) => Ok(SetInvocation::ProviderFailed {
exit_code,
summary: provider_failure_summary(provider, "__set", &output.stderr),
}),
Err(err) => Ok(SetInvocation::ProviderFailed {
exit_code: ExitCode::ContractViolation,
summary: format!("provider `{provider}` returned invalid set JSON: {err}"),
}),
}
}
pub fn invoke_go(
provider: &ProviderId,
capability: &str,
args: &[OsString],
contract: &EnvContract,
capture_json: bool,
) -> std::io::Result<GoInvocation> {
let Some(entry) = find_plugin(provider.as_str()) else {
return Ok(GoInvocation::ProviderUnavailable {
summary: format!("provider `{provider}` is not installed"),
});
};
let mut cmd = command_for_provider(&entry.binary_path, contract);
cmd.arg("__go").arg(capability).args(args);
if !capture_json {
let status = cmd.status()?;
return Ok(GoInvocation::Streamed {
exit_code: exit_code_from_status(status),
});
}
let output = cmd
.env("READY_SET_OUTPUT", "json")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
let exit_code = exit_code_from_status(output.status);
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<CapabilityRunReport>(stdout.trim()) {
Ok(report) => Ok(GoInvocation::Report { report, exit_code }),
Err(err) => Ok(GoInvocation::ProviderFailed {
exit_code: ExitCode::ContractViolation,
summary: format!("provider `{provider}` returned invalid go JSON: {err}"),
}),
}
}
fn command_for_provider(binary: &std::path::Path, contract: &EnvContract) -> Command {
let mut cmd = Command::new(binary);
export_contract(&mut cmd, contract);
cmd
}
fn provider_failure_summary(provider: &ProviderId, command: &str, stderr: &[u8]) -> String {
let stderr = String::from_utf8_lossy(stderr);
let detail = stderr.trim();
if detail.is_empty() {
format!("provider `{provider}` {command} failed")
} else {
format!("provider `{provider}` {command} failed: {detail}")
}
}
#[allow(clippy::match_same_arms)]
fn exit_code_from_status(status: ExitStatus) -> ExitCode {
if let Some(code) = status.code() {
return match code {
0 => ExitCode::Ok,
1 => ExitCode::UserError,
2 => ExitCode::SystemError,
3 => ExitCode::DependencyMissing,
4 => ExitCode::NotCargoWorkspace,
5 => ExitCode::ContractViolation,
127 => ExitCode::UnknownSubcommand,
_ => ExitCode::SystemError,
};
}
signaled_or_system(status)
}
#[cfg(unix)]
fn signaled_or_system(status: ExitStatus) -> ExitCode {
use std::os::unix::process::ExitStatusExt;
status
.signal()
.and_then(|s| u8::try_from(s).ok())
.map_or(ExitCode::SystemError, ExitCode::Signaled)
}
#[cfg(not(unix))]
const fn signaled_or_system(_status: ExitStatus) -> ExitCode {
ExitCode::SystemError
}
#[cfg(all(test, unix))]
mod tests {
use std::os::unix::process::ExitStatusExt;
use super::*;
fn exited(code: i32) -> ExitStatus {
ExitStatus::from_raw(code << 8)
}
fn signaled(signum: i32) -> ExitStatus {
ExitStatus::from_raw(signum)
}
#[test]
fn maps_documented_exit_codes() {
assert_eq!(exit_code_from_status(exited(0)), ExitCode::Ok);
assert_eq!(exit_code_from_status(exited(1)), ExitCode::UserError);
assert_eq!(exit_code_from_status(exited(2)), ExitCode::SystemError);
assert_eq!(
exit_code_from_status(exited(3)),
ExitCode::DependencyMissing
);
assert_eq!(
exit_code_from_status(exited(4)),
ExitCode::NotCargoWorkspace
);
assert_eq!(
exit_code_from_status(exited(5)),
ExitCode::ContractViolation
);
assert_eq!(
exit_code_from_status(exited(127)),
ExitCode::UnknownSubcommand
);
}
#[test]
fn unrecognized_exit_code_falls_back_to_system_error() {
assert_eq!(exit_code_from_status(exited(99)), ExitCode::SystemError);
assert_eq!(exit_code_from_status(exited(42)), ExitCode::SystemError);
}
#[test]
fn maps_signals_to_signaled_variant() {
assert_eq!(exit_code_from_status(signaled(2)), ExitCode::Signaled(2));
assert_eq!(exit_code_from_status(signaled(9)), ExitCode::Signaled(9));
assert_eq!(exit_code_from_status(signaled(15)), ExitCode::Signaled(15));
}
#[test]
fn signaled_emits_posix_shell_exit_code() {
assert_eq!(exit_code_from_status(signaled(2)).as_u8(), 130);
assert_eq!(exit_code_from_status(signaled(15)).as_u8(), 143);
}
}