use std::path::{Path, PathBuf};
use aion_awl::command::recipe::{CommandLine, CommandRunner, SpecimenFirer, VerdictRecord};
const CAUSE_MARKER: &str = "named cause: ";
const FIRED_LAYER: &str = "gate";
pub(crate) struct ProcessFirer {
root: PathBuf,
}
impl ProcessFirer {
pub(crate) fn rooted(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
}
}
}
impl SpecimenFirer for ProcessFirer {
fn fire(&self, address: &str) -> Result<VerdictRecord, String> {
let path = self.root.join(address);
if !path.exists() {
return Err(format!("{} does not exist", path.display()));
}
let output = std::process::Command::new(&path)
.current_dir(&self.root)
.output()
.map_err(|error| format!("{} could not be run: {error}", path.display()))?;
let mut said = String::from_utf8_lossy(&output.stdout).into_owned();
said.push_str(&String::from_utf8_lossy(&output.stderr));
let Some((code, line)) = named_cause(&said) else {
return Err(format!(
"{} exited {} and its output names no `{CAUSE_MARKER}<code>`, so there is nothing \
to compare a known answer against — an artifact fired as a known-answer red must \
say WHAT it found, not merely that it found something",
path.display(),
output
.status
.code()
.map_or_else(|| "by signal".to_owned(), |code| code.to_string())
));
};
VerdictRecord::new(code, FIRED_LAYER, line).map_err(|error| error.to_string())
}
}
fn named_cause(said: &str) -> Option<(String, String)> {
said.lines().find_map(|line| {
let at = line.find(CAUSE_MARKER)? + CAUSE_MARKER.len();
let code: String = line[at..]
.chars()
.take_while(|character| character.is_ascii_lowercase() || *character == '_')
.collect();
(!code.is_empty()).then(|| (code, line.trim().to_owned()))
})
}
pub(crate) struct ProcessRunner {
root: PathBuf,
}
impl ProcessRunner {
pub(crate) fn rooted(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
}
}
}
impl CommandRunner for ProcessRunner {
fn run(&self, line: &CommandLine<'_>) -> Result<i32, String> {
let Some((program, arguments)) = line.argv.split_first() else {
return Err("the emitted argv is empty, so there is no program to run".to_owned());
};
let mut process = std::process::Command::new(program);
process.args(arguments);
aion_worker::shell::place_in_declared_world(
&mut process,
line.env
.iter()
.map(|(name, value)| (name.as_str(), value.as_str())),
);
let working = line.cwd.map_or_else(
|| self.root.clone(),
|cwd| {
self.root
.join(cwd.replace(aion_awl::WORKSPACE_ROOT_PLACEHOLDER, "."))
},
);
process.current_dir(working);
let status = process
.status()
.map_err(|error| format!("`{program}` could not be run: {error}"))?;
status.code().ok_or_else(|| {
format!("`{program}` was ended by a signal rather than exiting, so it named no status")
})
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::Path;
use aion_awl::command::recipe::{CommandLine, CommandRunner};
use super::{ProcessRunner, named_cause};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const SHELL_OWN_NAMES: [&str; 3] = ["PWD", "SHLVL", "_"];
fn probe_script() -> String {
[
"{ /usr/bin/env",
"printf 'CWD=%s\\n' \"$(pwd)\"",
"printf 'ARGC=%s\\n' \"$#\"",
"printf 'ARG1=[%s]\\n' \"$1\"",
"printf 'STDIN=[%s]\\n' \"$(cat)\"",
"} > \"$2\" 2>&1",
]
.join("; ")
}
fn probe_argv(hostile: &str, observations: &Path) -> Vec<String> {
vec![
"/bin/sh".to_owned(),
"-c".to_owned(),
probe_script(),
"probe".to_owned(),
hostile.to_owned(),
observations.display().to_string(),
]
}
fn observed(
observations: &Path,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(observations)?;
Ok(text
.lines()
.filter_map(|line| line.split_once('='))
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect())
}
fn read<'a>(
observations: &'a BTreeMap<String, String>,
name: &str,
) -> Result<&'a str, Box<dyn std::error::Error>> {
observations
.get(name)
.map(String::as_str)
.ok_or_else(|| format!("the probe wrote no `{name}`: {observations:?}").into())
}
#[test]
fn a_fired_line_runs_in_the_world_the_worker_executor_establishes() -> TestResult {
let directory = tempfile::tempdir()?;
let workspace = directory.path().join("workspace");
std::fs::create_dir(&workspace)?;
let observations = directory.path().join("observed");
let hostile = "hostile value; echo PWNED";
let exports = vec![
("SEAT".to_owned(), "calliope".to_owned()),
("GREETING".to_owned(), "hello there".to_owned()),
];
let runner = ProcessRunner::rooted(&workspace);
let argv = probe_argv(hostile, &observations);
let status = runner.run(&CommandLine {
argv: &argv,
env: &exports,
cwd: None,
})?;
assert_eq!(status, 0, "the probe exits zero");
let seen = observed(&observations)?;
assert_eq!(read(&seen, "ARGC")?, "2");
assert_eq!(read(&seen, "ARG1")?, format!("[{hostile}]"));
assert_eq!(read(&seen, "STDIN")?, "[]");
let declared: Vec<&str> = vec!["PATH", "SEAT", "GREETING"];
let leaked: Vec<&str> = seen
.keys()
.map(String::as_str)
.filter(|name| {
!declared.contains(name)
&& !SHELL_OWN_NAMES.contains(name)
&& !["CWD", "ARGC", "ARG1", "STDIN"].contains(name)
})
.collect();
assert!(
leaked.is_empty(),
"the host's environment crossed into a fired line: {leaked:?}"
);
if let Some(present) = host_variable_other_than_path() {
assert!(
!seen.contains_key(&present),
"the host's `{present}` leaked into a fired line"
);
} else {
tracing::info!(
"skipping the by-name leak arm: the host holds no variable besides PATH and the \
shell's own to prove non-inheritance with"
);
}
match std::env::var("PATH") {
Ok(path) => assert_eq!(read(&seen, "PATH")?, path),
Err(error) => tracing::info!(
%error,
"skipping the PATH arm: this host holds no readable PATH to inherit"
),
}
assert_eq!(read(&seen, "SEAT")?, "calliope");
assert_eq!(read(&seen, "GREETING")?, "hello there");
assert_eq!(
Path::new(read(&seen, "CWD")?).canonicalize()?,
workspace.canonicalize()?
);
Ok(())
}
#[test]
fn the_stdin_probe_reports_bytes_when_a_pipe_carries_any() -> TestResult {
let directory = tempfile::tempdir()?;
let observations = directory.path().join("observed");
let argv = probe_argv("unused", &observations);
let (program, arguments) = argv.split_first().ok_or("the probe argv names a program")?;
let mut child = std::process::Command::new(program)
.args(arguments)
.stdin(std::process::Stdio::piped())
.spawn()?;
child
.stdin
.take()
.ok_or("the piped standard input must be available")?
.write_all(b"LEAKED")?;
let status = child.wait()?;
assert!(status.success(), "the probe exits zero");
assert_eq!(read(&observed(&observations)?, "STDIN")?, "[LEAKED]");
Ok(())
}
#[test]
fn an_exported_path_wins_over_the_inherited_one() -> TestResult {
let directory = tempfile::tempdir()?;
let observations = directory.path().join("observed");
let exports = vec![("PATH".to_owned(), "/usr/bin:/bin".to_owned())];
let runner = ProcessRunner::rooted(directory.path());
let argv = probe_argv("unused", &observations);
let status = runner.run(&CommandLine {
argv: &argv,
env: &exports,
cwd: None,
})?;
assert_eq!(status, 0);
assert_eq!(read(&observed(&observations)?, "PATH")?, "/usr/bin:/bin");
Ok(())
}
#[test]
fn a_declared_working_directory_is_resolved_against_this_runs_root() -> TestResult {
let directory = tempfile::tempdir()?;
let nested = directory.path().join("agents");
std::fs::create_dir(&nested)?;
let observations = directory.path().join("observed");
let cwd = format!("{}/agents", aion_awl::WORKSPACE_ROOT_PLACEHOLDER);
let runner = ProcessRunner::rooted(directory.path());
let argv = probe_argv("unused", &observations);
let status = runner.run(&CommandLine {
argv: &argv,
env: &[],
cwd: Some(&cwd),
})?;
assert_eq!(status, 0);
assert_eq!(
Path::new(read(&observed(&observations)?, "CWD")?).canonicalize()?,
nested.canonicalize()?
);
Ok(())
}
#[test]
fn the_status_is_the_lines_own_and_a_signal_death_names_none() -> TestResult {
let directory = tempfile::tempdir()?;
let runner = ProcessRunner::rooted(directory.path());
let exited = vec!["/bin/sh".to_owned(), "-c".to_owned(), "exit 7".to_owned()];
assert_eq!(
runner.run(&CommandLine {
argv: &exited,
env: &[],
cwd: None,
})?,
7
);
let signalled = vec![
"/bin/sh".to_owned(),
"-c".to_owned(),
"kill -TERM $$".to_owned(),
];
let Err(refusal) = runner.run(&CommandLine {
argv: &signalled,
env: &[],
cwd: None,
}) else {
return Err("a line ended by a signal names no status".into());
};
assert!(refusal.contains("signal"), "{refusal}");
let empty: Vec<String> = Vec::new();
let Err(refusal) = runner.run(&CommandLine {
argv: &empty,
env: &[],
cwd: None,
}) else {
return Err("an empty argv names no program to run".into());
};
assert!(refusal.contains("no program to run"), "{refusal}");
Ok(())
}
fn host_variable_other_than_path() -> Option<String> {
std::env::vars_os()
.filter_map(|(name, _)| name.into_string().ok())
.find(|name| {
name != "PATH"
&& !name.is_empty()
&& !name.contains('=')
&& !SHELL_OWN_NAMES.contains(&name.as_str())
})
}
#[test]
fn a_named_cause_is_read_out_of_the_shapes_this_estate_prints() {
assert_eq!(
named_cause(
"GATE-RED: no changed file under crates/aion-awl/ — the census would have passed \
vacuously (named cause: deliverable_absent)"
)
.map(|(code, _)| code),
Some("deliverable_absent".to_owned())
);
assert_eq!(
named_cause("command `c` states nothing (observed: \"c\"; named cause: body_empty)")
.map(|(code, _)| code),
Some("body_empty".to_owned())
);
}
#[test]
fn an_output_naming_no_cause_is_read_as_nothing() {
assert_eq!(named_cause("everything is fine"), None);
assert_eq!(named_cause(""), None);
assert_eq!(named_cause("named cause: "), None);
assert_eq!(named_cause("named cause: 404"), None);
}
#[test]
fn the_line_the_cause_stood_on_is_the_bytes_observed() {
let Some((_, line)) = named_cause(" GATE-RED: x (named cause: tree_dirty) ") else {
unreachable!("the line names a cause");
};
assert_eq!(line, "GATE-RED: x (named cause: tree_dirty)");
}
}