use crate::config::ResolvedServer;
use anyhow::{Result, anyhow};
use std::time::{Duration, Instant};
pub fn run_hook(path: &str, server: &ResolvedServer) -> Result<()> {
if path.is_empty() {
return Ok(());
}
let expanded = shellexpand::tilde(path).into_owned();
let mode = format!("{:?}", server.default_mode);
let mut child = std::process::Command::new(&expanded)
.env("SUSSHI_SERVER", &server.name)
.env("SUSSHI_HOST", &server.host)
.env("SUSSHI_USER", &server.user)
.env("SUSSHI_PORT", server.port.to_string())
.env("SUSSHI_MODE", &mode)
.spawn()
.map_err(|e| anyhow!("hook {expanded}: impossible de lancer : {e}"))?;
let timeout = Duration::from_secs(server.hook_timeout_secs);
let start = Instant::now();
loop {
match child.try_wait()? {
Some(status) if status.success() => return Ok(()),
Some(status) => {
return Err(anyhow!(
"hook {expanded}: exit code {:?}",
status.code().unwrap_or(-1)
));
}
None if start.elapsed() >= timeout => {
let _ = child.kill();
return Err(anyhow!(
"hook {expanded}: timeout ({}s)",
server.hook_timeout_secs
));
}
None => std::thread::sleep(Duration::from_millis(50)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ConnectionMode, ResolvedServer};
fn base_server() -> ResolvedServer {
ResolvedServer {
namespace: String::new(),
group_name: String::new(),
env_name: String::new(),
name: "srv".into(),
host: "198.51.100.1".into(),
user: "admin".into(),
port: 22,
ssh_key: String::new(),
ssh_options: vec![],
default_mode: ConnectionMode::Direct,
jump_host: None,
bastion_host: None,
bastion_user: None,
bastion_template: String::new(),
use_system_ssh_config: false,
probe_filesystems: vec![],
tunnels: vec![],
tags: vec![],
control_master: false,
control_path: String::new(),
control_persist: "10m".to_string(),
pre_connect_hook: None,
post_disconnect_hook: None,
hook_timeout_secs: 5,
wallix_group: None,
wallix_account: "default".to_string(),
wallix_protocol: "SSH".to_string(),
wallix_auto_select: true,
wallix_fail_if_menu_match_error: true,
wallix_selection_timeout_secs: 8,
}
}
#[test]
fn empty_path_is_noop() {
let s = base_server();
assert!(run_hook("", &s).is_ok());
}
#[test]
fn nonexistent_script_returns_err() {
let s = base_server();
let result = run_hook("/tmp/__susshi_nonexistent_hook_xyz.sh", &s);
assert!(result.is_err());
}
#[test]
fn script_exit_zero_is_ok() {
let s = base_server();
assert!(run_hook("/usr/bin/true", &s).is_ok());
}
#[test]
fn script_exit_nonzero_is_err() {
let s = base_server();
assert!(run_hook("/usr/bin/false", &s).is_err());
}
#[test]
fn timeout_kills_slow_script() {
let mut s = base_server();
s.hook_timeout_secs = 1;
let result = run_hook("/usr/bin/sleep", &s);
let _ = result; }
}