use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
use signal_hook::consts::{SIGINT, SIGTERM};
use signal_hook::iterator::Signals;
static INSTALLED: AtomicBool = AtomicBool::new(false);
static CHILD_PIDS: Mutex<Vec<u32>> = Mutex::new(Vec::new());
static CONTAINERS: Mutex<Vec<(String, String)>> = Mutex::new(Vec::new());
pub fn install() {
if INSTALLED.swap(true, Ordering::SeqCst) {
return;
}
let mut signals = match Signals::new([SIGINT, SIGTERM]) {
Ok(s) => s,
Err(_) => return, };
thread::spawn(move || {
for sig in signals.forever() {
super::screen::restore_terminal();
terminate_all();
std::process::exit(128 + sig);
}
});
}
pub fn isolate_child(cmd: &mut Command) {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0); }
#[cfg(not(unix))]
let _ = cmd;
}
pub fn register_child(pid: u32) {
if let Ok(mut v) = CHILD_PIDS.lock() {
v.push(pid);
}
}
pub fn unregister_child(pid: u32) {
if let Ok(mut v) = CHILD_PIDS.lock() {
v.retain(|&p| p != pid);
}
}
pub fn register_container(runtime: &str, name: &str) {
if let Ok(mut v) = CONTAINERS.lock() {
v.push((runtime.to_string(), name.to_string()));
}
}
pub fn unregister_container(runtime: &str, name: &str) {
if let Ok(mut v) = CONTAINERS.lock() {
v.retain(|(r, n)| !(r == runtime && n == name));
}
}
pub struct ContainerGuard {
runtime: String,
name: String,
}
impl ContainerGuard {
pub fn new(runtime: &str, name: &str) -> ContainerGuard {
register_container(runtime, name);
ContainerGuard { runtime: runtime.to_string(), name: name.to_string() }
}
}
impl Drop for ContainerGuard {
fn drop(&mut self) {
unregister_container(&self.runtime, &self.name);
}
}
pub fn terminate_children() {
let pids = CHILD_PIDS.lock().map(|v| v.clone()).unwrap_or_default();
for pid in &pids {
let _ = Command::new("kill").arg("-TERM").arg(pid.to_string()).status();
}
if !pids.is_empty() {
thread::sleep(Duration::from_secs(1));
}
for pid in &pids {
let _ = Command::new("kill").arg("-9").arg(pid.to_string()).status();
}
}
pub fn stop_container(runtime: &str, name: &str) {
signal_container(runtime, name, "TERM");
thread::sleep(Duration::from_secs(1));
signal_container(runtime, name, "KILL");
}
fn signal_container(runtime: &str, name: &str, sig: &str) {
let _ = Command::new(runtime)
.args(["kill", "-s", sig, name])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
pub fn terminate_all() {
let containers = CONTAINERS.lock().map(|v| v.clone()).unwrap_or_default();
terminate_children();
for (runtime, name) in &containers {
signal_container(runtime, name, "TERM");
}
if !containers.is_empty() {
thread::sleep(Duration::from_secs(1));
}
for (runtime, name) in &containers {
signal_container(runtime, name, "KILL");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn isolated_child_runs_in_its_own_process_group() {
fn pgid(stat_path: &str) -> i32 {
let stat = std::fs::read_to_string(stat_path).unwrap();
let after = &stat[stat.rfind(')').unwrap() + 1..];
after.split_whitespace().nth(2).unwrap().parse().unwrap() }
let ours = pgid("/proc/self/stat");
let mut cmd = Command::new("sleep");
cmd.arg("30");
isolate_child(&mut cmd);
let mut child = cmd.spawn().expect("spawn sleep");
let id = child.id();
let theirs = pgid(&format!("/proc/{id}/stat"));
let _ = child.kill();
let _ = child.wait();
assert_eq!(theirs, id as i32, "the child should lead its own process group");
assert_ne!(theirs, ours, "the child must not share scsh's process group");
}
#[test]
fn register_and_unregister_track_children() {
let fake = 4_242_424; register_child(fake);
assert!(CHILD_PIDS.lock().unwrap().contains(&fake));
unregister_child(fake);
assert!(!CHILD_PIDS.lock().unwrap().contains(&fake));
}
#[test]
fn register_and_unregister_track_containers() {
register_container("docker", "scsh-test");
assert!(CONTAINERS.lock().unwrap().iter().any(|(r, n)| r == "docker" && n == "scsh-test"));
unregister_container("docker", "scsh-test");
assert!(!CONTAINERS.lock().unwrap().iter().any(|(r, n)| r == "docker" && n == "scsh-test"));
}
#[test]
fn container_guard_unregisters_on_drop() {
{
let _g = ContainerGuard::new("docker", "scsh-guard-test");
assert!(CONTAINERS.lock().unwrap().iter().any(|(_, n)| n == "scsh-guard-test"));
}
assert!(!CONTAINERS.lock().unwrap().iter().any(|(_, n)| n == "scsh-guard-test"));
}
}