#![cfg(unix)]
#![cfg(test)]
#![allow(clippy::panic_in_result_fn, clippy::expect_used)]
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use anyhow::Result;
use shrub_core::options::{SpawnOptions, SpawnedProcess};
const BACKGROUND_WAIT: Duration = Duration::from_secs(10);
async fn new_shell(spawn: SpawnOptions) -> Result<shrub_core::Shell> {
let mut shell = shrub_core::Shell::builder()
.do_not_inherit_env(true)
.skip_well_known_vars(true)
.kill_external_commands_on_drop(true)
.build()
.await?;
shell.options_mut().spawn = spawn;
Ok(shell)
}
fn join_file(path: &Path) -> Arc<CString> {
Arc::new(CString::new(path.as_os_str().as_bytes()).expect("a path without a NUL byte"))
}
async fn run(shell: &mut shrub_core::Shell, script: &str) -> Result<shrub_core::ExecutionResult> {
let params = shell.default_exec_params();
Ok(shell
.run_string(script, &shrub_core::SourceInfo::default(), ¶ms)
.await?)
}
fn pids(path: &Path) -> Result<Vec<i32>> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(error) => return Err(error.into()),
};
Ok(text.lines().map(str::parse).collect::<Result<_, _>>()?)
}
#[tokio::test(flavor = "multi_thread")]
async fn the_observer_sees_every_external_process_with_its_pid() -> Result<()> {
let dir = tempfile::tempdir()?;
let file = dir.path().join("pids");
let seen: Arc<Mutex<Vec<SpawnedProcess>>> = Arc::default();
let mut shell = new_shell(SpawnOptions {
on_spawn: Some(Arc::new({
let seen = Arc::clone(&seen);
move |process| seen.lock().expect("the list's lock").push(process)
})),
..SpawnOptions::default()
})
.await?;
let script = format!(
"unused=1; \
/bin/sh -c 'echo $$' >> {file}; \
/bin/sh -c 'echo $$' >> {file} | /bin/sh -c 'echo $$ >> {file}'; \
/bin/sh -c 'echo $$' >> {file} &",
file = file.display(),
);
let result = run(&mut shell, &script).await?;
assert!(result.is_success());
let began = Instant::now();
while pids(&file)?.len() < 4 && began.elapsed() < BACKGROUND_WAIT {
tokio::time::sleep(Duration::from_millis(10)).await;
}
let mut printed = pids(&file)?;
let mut observed: Vec<i32> = seen
.lock()
.expect("the list's lock")
.iter()
.map(|process| process.pid)
.collect();
printed.sort_unstable();
observed.sort_unstable();
assert_eq!(printed.len(), 4);
assert_eq!(observed, printed);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn each_child_writes_zero_to_the_join_file_before_its_program_runs() -> Result<()> {
let dir = tempfile::tempdir()?;
let procs = dir.path().join("cgroup.procs");
let seen = dir.path().join("seen");
std::fs::write(&procs, "x")?;
let mut shell = new_shell(SpawnOptions {
cgroup_procs: Some(join_file(&procs)),
..SpawnOptions::default()
})
.await?;
let script = format!("/bin/cat {} > {}", procs.display(), seen.display());
let result = run(&mut shell, &script).await?;
assert!(result.is_success());
assert_eq!(std::fs::read_to_string(&seen)?, "0");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_join_file_that_cannot_be_opened_fails_the_command_and_runs_nothing() -> Result<()> {
let dir = tempfile::tempdir()?;
let marker = dir.path().join("marker");
let mut shell = new_shell(SpawnOptions {
cgroup_procs: Some(join_file(&dir.path().join("missing").join("cgroup.procs"))),
..SpawnOptions::default()
})
.await?;
let script = format!("/usr/bin/touch {}", marker.display());
let result = run(&mut shell, &script).await;
if let Ok(result) = result {
assert!(
!result.is_success(),
"the command succeeded without its join file"
);
assert_ne!(
u8::from(result.exit_code),
127,
"reported as command not found"
);
}
assert!(!marker.exists(), "the program ran outside its cgroup");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn without_spawn_options_a_command_runs_as_before() -> Result<()> {
let dir = tempfile::tempdir()?;
let marker = dir.path().join("marker");
let mut shell = new_shell(SpawnOptions::default()).await?;
let script = format!("/usr/bin/touch {}", marker.display());
let result = run(&mut shell, &script).await?;
assert!(result.is_success());
assert!(marker.exists());
Ok(())
}