use std::process::Command;
use crate as cindy;
use crate::Context;
#[derive(Clone, PartialEq, Eq)]
#[crate::wire]
pub struct Output {
pub code: Option<i32>,
pub stdout: String,
pub stderr: String,
}
impl Output {
pub fn success(&self) -> bool {
self.code == Some(0)
}
}
#[derive(Clone, Default, PartialEq, Eq)]
#[crate::wire]
pub struct State {
pub chdir: Option<std::path::PathBuf>,
pub env: std::collections::BTreeMap<String, String>,
pub ignore_errors: bool,
}
fn run(mut cmd: Command, state: &State, label: &str) -> crate::Result<Output> {
if let Some(dir) = state.chdir.as_ref() {
cmd.current_dir(dir);
}
for (k, v) in &state.env {
cmd.env(k, v);
}
let out = cmd.output().context(format!("Failed to spawn {label}"))?;
let code = out.status.code();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
if !out.status.success() && !state.ignore_errors {
crate::bail!(
"{label} failed with {:?}:\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}",
out.status,
);
}
Ok(Output {
code,
stdout,
stderr,
})
}
#[crate::remote]
pub fn command(argv: Vec<String>, state: State) -> crate::Result<Output> {
let Some((program, args)) = argv.split_first() else {
crate::bail!("`command` requires a non-empty argv (argv[0] is the program)");
};
let mut cmd = Command::new(program);
cmd.args(args);
run(cmd, &state, &format!("`command` {program:?}"))
}
#[crate::remote]
pub fn shell(
interpreter: std::path::PathBuf,
script: String,
state: State,
) -> crate::Result<Output> {
if !interpreter.is_absolute() {
crate::bail!(
"`shell` interpreter must be an absolute path, got {}; \
there is no implicit shell — spell out e.g. /bin/bash",
interpreter.display()
);
}
if !interpreter.exists() {
crate::bail!(
"`shell` interpreter {} does not exist on the remote",
interpreter.display()
);
}
let mut cmd = Command::new(&interpreter);
cmd.arg("-c").arg(&script);
run(
cmd,
&state,
&format!("`shell` via {}", interpreter.display()),
)
}