1use std::ffi::OsStr;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::process::{Command, Output, Stdio};
5use std::time::{Duration, Instant};
6
7use crate::error::{Error, Result};
8
9const CHILD_TIMEOUT: Duration = Duration::from_secs(30);
17
18#[derive(Debug, Clone)]
25pub struct Tools {
26 pub kinit: PathBuf,
27 pub klist: PathBuf,
28 pub kdestroy: PathBuf,
29 pub realm: PathBuf,
30 pub sssctl: PathBuf,
31 pub getent: PathBuf,
32}
33
34impl Default for Tools {
35 fn default() -> Self {
36 Self {
37 kinit: "/usr/bin/kinit".into(),
38 klist: "/usr/bin/klist".into(),
39 kdestroy: "/usr/bin/kdestroy".into(),
40 realm: "/usr/bin/realm".into(),
41 sssctl: "/usr/bin/sssctl".into(),
42 getent: "/usr/bin/getent".into(),
43 }
44 }
45}
46
47const SCRUBBED_ENV: &[&str] = &[
52 "KRB5_CONFIG",
53 "KRB5CCNAME",
54 "KRB5_KTNAME",
55 "KRB5_CLIENT_KTNAME",
56 "KRB5_TRACE",
57 "KRB5RCACHEDIR",
58 "KRB5RCACHETYPE",
59];
60
61pub(crate) fn run<I, S>(program: &Path, args: I) -> Result<Output>
64where
65 I: IntoIterator<Item = S>,
66 S: AsRef<OsStr>,
67{
68 run_with_timeout(program, args, CHILD_TIMEOUT)
69}
70
71fn run_with_timeout<I, S>(program: &Path, args: I, timeout: Duration) -> Result<Output>
72where
73 I: IntoIterator<Item = S>,
74 S: AsRef<OsStr>,
75{
76 let name = || program.display().to_string();
77 let mut cmd = Command::new(program);
80 cmd.args(args)
81 .env("LC_ALL", "C")
82 .stdin(Stdio::null())
83 .stdout(Stdio::piped())
84 .stderr(Stdio::piped());
85 for var in SCRUBBED_ENV {
86 cmd.env_remove(var);
87 }
88 let mut child = cmd.spawn().map_err(|source| Error::Spawn {
89 program: name(),
90 source,
91 })?;
92
93 let deadline = Instant::now() + timeout;
94 let status = loop {
95 match child.try_wait() {
96 Ok(Some(status)) => break status,
97 Ok(None) if Instant::now() >= deadline => {
98 let _ = child.kill();
99 let _ = child.wait(); return Err(Error::Timeout {
101 program: name(),
102 seconds: timeout.as_secs(),
103 });
104 }
105 Ok(None) => std::thread::sleep(Duration::from_millis(50)),
106 Err(source) => {
107 let _ = child.kill();
108 let _ = child.wait();
109 return Err(Error::Spawn {
110 program: name(),
111 source,
112 });
113 }
114 }
115 };
116
117 let mut stdout = Vec::new();
120 let mut stderr = Vec::new();
121 if let Some(mut out) = child.stdout.take() {
122 let _ = out.read_to_end(&mut stdout);
123 }
124 if let Some(mut err) = child.stderr.take() {
125 let _ = err.read_to_end(&mut stderr);
126 }
127 Ok(Output {
128 status,
129 stdout,
130 stderr,
131 })
132}
133
134pub(crate) fn run_ok<I, S>(program: &Path, args: I) -> Result<String>
136where
137 I: IntoIterator<Item = S>,
138 S: AsRef<OsStr>,
139{
140 let out = run(program, args)?;
141 if out.status.success() {
142 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
143 } else {
144 Err(Error::CommandFailed {
145 program: program.display().to_string(),
146 status: out
147 .status
148 .code()
149 .map_or_else(|| "killed by signal".to_string(), |c| format!("exit {c}")),
150 stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
151 })
152 }
153}
154
155pub(crate) fn succeeds<I, S>(program: &Path, args: I) -> bool
157where
158 I: IntoIterator<Item = S>,
159 S: AsRef<OsStr>,
160{
161 run(program, args)
162 .map(|o| o.status.success())
163 .unwrap_or(false)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn hung_child_is_killed_and_reported() {
172 let started = Instant::now();
173 let err = run_with_timeout(
174 Path::new("/usr/bin/sleep"),
175 ["30"],
176 Duration::from_millis(300),
177 )
178 .unwrap_err();
179 assert!(matches!(err, Error::Timeout { .. }), "{err}");
180 assert!(started.elapsed() < Duration::from_secs(5));
182 }
183
184 #[test]
185 fn fast_child_completes_with_output() {
186 let out = run_with_timeout(
187 Path::new("/usr/bin/echo"),
188 ["hello"],
189 Duration::from_secs(10),
190 )
191 .unwrap();
192 assert!(out.status.success());
193 assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello");
194 }
195}