use std::ffi::OsStr;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::{Duration, Instant};
use crate::error::{Error, Result};
const CHILD_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct Tools {
pub kinit: PathBuf,
pub klist: PathBuf,
pub kdestroy: PathBuf,
pub realm: PathBuf,
pub sssctl: PathBuf,
pub getent: PathBuf,
}
impl Default for Tools {
fn default() -> Self {
Self {
kinit: "/usr/bin/kinit".into(),
klist: "/usr/bin/klist".into(),
kdestroy: "/usr/bin/kdestroy".into(),
realm: "/usr/bin/realm".into(),
sssctl: "/usr/bin/sssctl".into(),
getent: "/usr/bin/getent".into(),
}
}
}
const SCRUBBED_ENV: &[&str] = &[
"KRB5_CONFIG",
"KRB5CCNAME",
"KRB5_KTNAME",
"KRB5_CLIENT_KTNAME",
"KRB5_TRACE",
"KRB5RCACHEDIR",
"KRB5RCACHETYPE",
];
pub(crate) fn run<I, S>(program: &Path, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_with_timeout(program, args, CHILD_TIMEOUT)
}
fn run_with_timeout<I, S>(program: &Path, args: I, timeout: Duration) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let name = || program.display().to_string();
let mut cmd = Command::new(program);
cmd.args(args)
.env("LC_ALL", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for var in SCRUBBED_ENV {
cmd.env_remove(var);
}
let mut child = cmd.spawn().map_err(|source| Error::Spawn {
program: name(),
source,
})?;
let deadline = Instant::now() + timeout;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait(); return Err(Error::Timeout {
program: name(),
seconds: timeout.as_secs(),
});
}
Ok(None) => std::thread::sleep(Duration::from_millis(50)),
Err(source) => {
let _ = child.kill();
let _ = child.wait();
return Err(Error::Spawn {
program: name(),
source,
});
}
}
};
let mut stdout = Vec::new();
let mut stderr = Vec::new();
if let Some(mut out) = child.stdout.take() {
let _ = out.read_to_end(&mut stdout);
}
if let Some(mut err) = child.stderr.take() {
let _ = err.read_to_end(&mut stderr);
}
Ok(Output {
status,
stdout,
stderr,
})
}
pub(crate) fn run_ok<I, S>(program: &Path, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let out = run(program, args)?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
} else {
Err(Error::CommandFailed {
program: program.display().to_string(),
status: out
.status
.code()
.map_or_else(|| "killed by signal".to_string(), |c| format!("exit {c}")),
stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
})
}
}
pub(crate) fn succeeds<I, S>(program: &Path, args: I) -> bool
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run(program, args)
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hung_child_is_killed_and_reported() {
let started = Instant::now();
let err = run_with_timeout(
Path::new("/usr/bin/sleep"),
["30"],
Duration::from_millis(300),
)
.unwrap_err();
assert!(matches!(err, Error::Timeout { .. }), "{err}");
assert!(started.elapsed() < Duration::from_secs(5));
}
#[test]
fn fast_child_completes_with_output() {
let out = run_with_timeout(
Path::new("/usr/bin/echo"),
["hello"],
Duration::from_secs(10),
)
.unwrap();
assert!(out.status.success());
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello");
}
}