use crate::config::{ConnectionMode, ResolvedServer};
use crate::ssh::client::{build_ssh_args, build_wallix_direct_args};
use anyhow::Result;
use std::io::{Read, Write as _};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub const COMMAND_TIMEOUT: Duration = Duration::from_secs(120);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecOutput {
pub stdout: String,
pub stderr: String,
pub success: bool,
}
fn push_batch_options(args: &mut Vec<String>) {
for opt in ["BatchMode=yes", "ConnectTimeout=10", "LogLevel=ERROR"] {
args.push("-o".into());
args.push(opt.into());
}
}
pub fn build_exec_args(
server: &ResolvedServer,
mode: ConnectionMode,
command: &str,
) -> Result<Vec<String>> {
let mut args = build_ssh_args(server, mode, false)?;
let destination = args
.pop()
.ok_or_else(|| anyhow::anyhow!("liste d'args SSH vide"))?;
args.push("-n".into()); push_batch_options(&mut args);
args.push(destination);
args.push(command.to_string());
Ok(args)
}
pub fn build_wallix_exec_args(
server: &ResolvedServer,
authorization: Option<&str>,
) -> Result<Vec<String>> {
let mut args = build_wallix_direct_args(server, authorization)?;
let destination = args
.pop()
.ok_or_else(|| anyhow::anyhow!("liste d'args SSH vide"))?;
args.push("-T".into());
push_batch_options(&mut args);
args.push(destination);
Ok(args)
}
pub fn run_remote(
server: &ResolvedServer,
mode: ConnectionMode,
command: &str,
timeout: Duration,
) -> Result<ExecOutput> {
if mode != ConnectionMode::Wallix {
let args = build_exec_args(server, mode, command)?;
return run_process("ssh", &args, None, timeout);
}
let mut refused: Vec<String> = Vec::new();
for authorization in crate::wallix::fallback_authorizations(server) {
let args = build_wallix_exec_args(server, authorization.as_deref())?;
let output = run_process("ssh", &args, Some(command), timeout)?;
if !output.success && crate::wallix::is_invalid_target(&output.stderr) {
refused.push(crate::wallix::build_direct_login(
server,
authorization.as_deref(),
));
continue;
}
return Ok(output);
}
anyhow::bail!(
"Wallix : cible invalide pour toutes les autorisations essayées [{}] — \
déclarez la bonne dans wallix.authorizations",
refused.join(", ")
)
}
pub(crate) const END_MARKER: &str = "__SUSSHI_END__";
fn shell_input(command: &str) -> String {
format!("{command}\necho \"{END_MARKER}$?\"\nexit\n")
}
fn split_end_marker(stdout: &str) -> Option<(String, i32)> {
let pos = stdout.rfind(END_MARKER)?;
let rest = &stdout[pos + END_MARKER.len()..];
let (code, _) = rest.split_once('\n')?;
let code = code.trim().parse().ok()?;
Some((stdout[..pos].to_string(), code))
}
fn spawn_reader<R: Read + Send + 'static>(
mut source: R,
) -> (Arc<Mutex<Vec<u8>>>, std::thread::JoinHandle<()>) {
let buffer = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&buffer);
let handle = std::thread::spawn(move || {
let mut chunk = [0u8; 4096];
while let Ok(n) = source.read(&mut chunk) {
if n == 0 {
break;
}
if let Ok(mut buf) = sink.lock() {
buf.extend_from_slice(&chunk[..n]);
}
}
});
(buffer, handle)
}
fn wait_readers(readers: &[&std::thread::JoinHandle<()>]) {
let deadline = Instant::now() + READER_GRACE;
while Instant::now() < deadline && !readers.iter().all(|r| r.is_finished()) {
std::thread::sleep(Duration::from_millis(10));
}
}
const READER_GRACE: Duration = Duration::from_millis(300);
fn snapshot(buffer: &Arc<Mutex<Vec<u8>>>) -> String {
buffer
.lock()
.map(|b| String::from_utf8_lossy(&b).into_owned())
.unwrap_or_default()
}
fn run_process(
program: &str,
args: &[String],
command: Option<&str>,
timeout: Duration,
) -> Result<ExecOutput> {
let mut child = Command::new(program)
.args(args)
.stdin(if command.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow::anyhow!("lancement de {program} : {e}"))?;
if let (Some(command), Some(mut stdin)) = (command, child.stdin.take()) {
let _ = stdin.write_all(shell_input(command).as_bytes());
}
let stdout_pipe = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("stdout de {program} indisponible"))?;
let stderr_pipe = child
.stderr
.take()
.ok_or_else(|| anyhow::anyhow!("stderr de {program} indisponible"))?;
let (stdout_buf, stdout_reader) = spawn_reader(stdout_pipe);
let (stderr_buf, stderr_reader) = spawn_reader(stderr_pipe);
let deadline = Instant::now() + timeout;
let mut marker: Option<(String, i32)> = None;
let status = loop {
if let Some(status) = child.try_wait()? {
break Some(status);
}
if command.is_some() {
marker = split_end_marker(&snapshot(&stdout_buf));
if marker.is_some() {
let _ = child.kill();
let _ = child.wait();
break None;
}
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
wait_readers(&[&stdout_reader, &stderr_reader]);
let partial = format!("{}{}", snapshot(&stdout_buf), snapshot(&stderr_buf));
anyhow::bail!(
"délai dépassé ({}s) sans fin de commande : {}",
timeout.as_secs(),
partial.trim()
);
}
std::thread::sleep(Duration::from_millis(50));
};
wait_readers(&[&stdout_reader, &stderr_reader]);
let stdout = snapshot(&stdout_buf);
let stderr = snapshot(&stderr_buf);
if marker.is_none() && command.is_some() {
marker = split_end_marker(&stdout);
}
Ok(match marker {
Some((output, code)) => ExecOutput {
stdout: output,
stderr,
success: code == 0,
},
None => ExecOutput {
stdout,
stderr,
success: status.is_some_and(|s| s.success()),
},
})
}
#[cfg(test)]
#[path = "exec_tests.rs"]
mod tests;