use core::str;
use std::{
path::Path,
time::{Duration, Instant},
};
use super::{error::*, pid::*, *};
pub fn kill_by_client(pidfile_path: &Path, host: &str) -> Result<()> {
let pid: Option<u32> = std::fs::read_to_string(pidfile_path)
.ok()
.and_then(|s| s.trim().parse().ok());
if let Some(pid) = pid {
match pid_alive(pid) {
Ok(true) => {
crate::info!("Killing server at {host} (PID {pid}) via pid-file");
let res = kill_pids(&[pid], POLITE_WAIT);
if res.is_ok() {
if let Err(e) = std::fs::remove_file(pidfile_path) {
crate::warn!("Failed to remove pid-file for host {host}: {e}");
}
}
return res;
}
Ok(false) => match std::fs::remove_file(pidfile_path) {
Ok(_) => (),
Err(e) => crate::warn!("Failed to remove stale pid-file for host {host}: {e}"),
},
Err(e) => {
crate::warn!("pid_alive({pid}) failed: {e}. Falling back to argv scan…");
}
}
} else {
if let Err(e) = std::fs::remove_file(pidfile_path) {
crate::warn!("Failed to remove malformed pid-file for host {host}: {e}");
}
};
let patterns: &[&[&str]] = &[&["--host", host], &["-h", host]];
if let Some(pid) = get_server_pid_by_cmd_args(patterns) {
crate::info!("Killing server at {host} (PID {pid}) via argv scan");
match kill_pids(&[pid], POLITE_WAIT) {
Ok(()) => {
match std::fs::remove_file(pidfile_path) {
Ok(_) => (),
Err(e) => {
crate::warn!("Failed to remove stale pid-file for host {host}: {e}")
}
}
return Ok(());
}
Err(e) => {
crate::warn!("Failed to kill server at {host} (PID {pid}): {e}");
}
}
}
Err(ProcessError::NoSuchProcess {
query: format!("host={host}"),
})
}
pub fn kill_all_servers(executable_name: &str) -> Result<()> {
crate::info!("Killing all {executable_name} processes");
let pids = get_all_server_pids(executable_name);
let mut errors = Vec::new();
if !pids.is_empty() {
if let Err(e) = kill_pids(&pids, POLITE_WAIT) {
errors.push(e);
}
}
for pid in pids {
match pid_alive(pid) {
Ok(false) => crate::info!("PID {pid} shut down"),
Ok(true) => crate::warn!("PID {pid} still alive, but we tried to kill it"),
Err(e) => crate::warn!("Could not probe PID {pid}: {e}"),
}
}
if errors.is_empty() {
Ok(())
} else {
for e in &errors[1..] {
crate::warn!("Additional error while killing servers: {e}");
}
Err(errors.remove(0))
}
}
fn kill_pids(pids: &[u32], polite_wait: Duration) -> Result<()> {
let mut seen = std::collections::HashSet::with_capacity(pids.len());
let uniq: Vec<u32> = pids.iter().copied().filter(|p| seen.insert(*p)).collect();
if uniq.is_empty() {
return Ok(());
}
let start = Instant::now();
for pid in &uniq {
match pid_alive(*pid) {
Ok(true) => match kill_pid(*pid) {
Ok(()) => crate::info!("Sent TERM to PID {}", pid),
Err(e) => crate::error!("Failed to send TERM to PID {}: {}", pid, e),
},
Ok(false) => (),
Err(e) => crate::error!("Failed to check PID {}: {}", pid, e),
}
}
let polite_deadline = Instant::now() + polite_wait;
let mut probe_failures: Vec<(u32, ProcessError)> = Vec::new();
while Instant::now() < polite_deadline {
let all_dead = pids.iter().all(|&pid| match pid_alive(pid) {
Ok(alive) => !alive,
Err(e) => {
if !probe_failures.iter().any(|(p, _)| *p == pid) {
probe_failures.push((pid, e));
}
false
}
});
if all_dead {
break;
}
std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
for &pid in pids {
if let Err(e) = force_kill_pid(pid) {
crate::error!("Failed to force-kill PID {pid}: {e}");
}
}
if !probe_failures.is_empty() {
for (pid, err) in &probe_failures {
crate::warn!("Never obtained status for PID {pid}: {err}");
}
}
let force_kill_deadline = Instant::now() + Duration::from_secs(FORCE_KILL_TIMEOUT_SECS);
while Instant::now() < force_kill_deadline {
if pids
.iter()
.all(|&pid| matches!(pid_alive(pid), Ok(false) | Err(_)))
{
break;
}
std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
#[cfg(target_os = "macos")]
for &pid in &uniq {
use nix::sys::wait::{waitpid, WaitPidFlag};
let _ = nix::unistd::Pid::from_raw(pid as i32);
let _ = waitpid(
nix::unistd::Pid::from_raw(pid as i32),
Some(WaitPidFlag::WNOHANG),
);
}
let leftovers: Vec<u32> = pids
.iter()
.copied()
.filter(|&pid| match pid_alive(pid) {
Ok(alive) => alive,
Err(_) => true, })
.collect();
let elapsed = start.elapsed();
if leftovers.is_empty() {
Ok(())
} else {
Err(ProcessError::TerminationTimeout {
operation: "kill_pids",
elapsed,
leftovers,
})
}
}
#[cfg(unix)]
pub fn kill_pid(pid: u32) -> Result<()> {
use nix::{
errno::Errno,
sys::signal::{kill, Signal},
unistd::Pid,
};
match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
Ok(_) | Err(Errno::ESRCH) => Ok(()),
Err(Errno::EPERM) => Err(ProcessError::PermissionDenied {
action: "send SIGTERM",
source: "operation not permitted".into(),
}),
Err(e) => Err(ProcessError::CommandFailed {
action: "send SIGTERM",
source: e.into(),
}),
}
}
#[cfg(unix)]
fn force_kill_pid(pid: u32) -> Result<()> {
use nix::{
errno::Errno,
sys::signal::{kill, Signal},
unistd::Pid,
};
match kill(Pid::from_raw(pid as i32), Signal::SIGKILL) {
Ok(_) | Err(Errno::ESRCH) => Ok(()),
Err(Errno::EPERM) => Err(ProcessError::PermissionDenied {
action: "send SIGKILL",
source: "operation not permitted".into(),
}),
Err(e) => Err(ProcessError::CommandFailed {
action: "send SIGKILL",
source: e.into(),
}),
}
}
#[cfg(windows)]
pub fn kill_pid(pid: u32) -> Result<()> {
use windows::Win32::{
Foundation::CloseHandle,
System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE},
};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, false, pid).map_err(|e| {
ProcessError::CommandFailed {
action: "OpenProcess",
source: Box::new(e),
}
})?;
if handle.is_invalid() {
return Ok(());
}
let result = TerminateProcess(handle, 1);
let _ = CloseHandle(handle);
result.map_err(|e| ProcessError::CommandFailed {
action: "TerminateProcess",
source: Box::new(e),
})
}
}
#[cfg(windows)]
pub fn force_kill_pid(pid: u32) -> Result<()> {
use windows::Win32::{
Foundation::CloseHandle,
System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE},
};
fn win32_error(action: &'static str) -> ProcessError {
let err = windows::core::Error::from_win32();
match err.code().0 {
5 => ProcessError::PermissionDenied {
action,
source: Box::new(err),
},
_ => ProcessError::CommandFailed {
action,
source: Box::new(err),
},
}
}
unsafe {
let h = OpenProcess(PROCESS_TERMINATE, false, pid).map_err(|e| {
ProcessError::CommandFailed {
action: "force-kill (OpenProcess)",
source: e.into(),
}
})?;
if h.is_invalid() {
let err = windows::core::Error::from_win32();
return match err.code().0 {
87 => Ok(()), _ => Err(win32_error("force-kill (OpenProcess)")),
};
}
match TerminateProcess(h, 1) {
Ok(_) => CloseHandle(h).map_err(|e| ProcessError::CommandFailed {
action: "force-kill (CloseHandle)",
source: e.into(),
}),
Err(_) => {
let _ = CloseHandle(h);
Err(win32_error("force-kill (TerminateProcess)"))
}
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use serial_test::serial;
use tempfile::tempdir;
use super::*;
use crate::server::process::tests_helpers::*;
#[test]
#[serial]
fn kill_pids_scenarios() {
use ProcessError::*;
assert!(kill_pids(&[], Duration::from_millis(10)).is_ok());
let dead_pid = {
let mut child = short_cmd().spawn().unwrap();
let pid = child.id();
let _ = child.wait();
pid
};
match kill_pids(&[dead_pid], Duration::from_millis(200)) {
Ok(()) | Err(TerminationTimeout { .. }) => {} Err(e) => panic!("unexpected error on all-dead slice: {e:?}"),
}
fn spawn_and_kill(wait: Duration, duplicate: bool) {
let mut child = long_cmd().spawn().unwrap();
let pid = child.id();
let pids = if duplicate { vec![pid, pid] } else { vec![pid] };
match kill_pids(&pids, wait) {
Ok(()) | Err(TerminationTimeout { .. }) => {}
Err(e) => panic!("kill_pids failed unexpectedly: {e:?}"),
}
let _ = child.wait();
assert!(
!pid_alive(pid).unwrap_or(true),
"child {pid} still alive after kill_pids(wait={wait:?}, dup={duplicate})"
);
}
spawn_and_kill(Duration::from_secs(2), true);
for &d in &[Duration::from_secs(2), Duration::from_secs(0)] {
spawn_and_kill(d, false);
}
let mut child_live = long_cmd().spawn().unwrap();
let pid_live = child_live.id();
let mut child_dead = long_cmd().spawn().unwrap();
let pid_dead = child_dead.id();
kill_pid(pid_dead).unwrap(); let _ = child_dead.wait();
match kill_pids(&[pid_dead, pid_live], Duration::from_millis(500)) {
Ok(()) | Err(TerminationTimeout { .. }) => {}
Err(e) => panic!("mixed kill failed unexpectedly: {e:?}"),
}
let _ = child_live.wait();
assert!(
!pid_alive(pid_live).unwrap_or(true),
"live child {pid_live} survived mixed-status kill"
);
}
#[test]
#[serial]
fn kill_by_client_scenarios() {
use sanitize_filename::sanitize;
struct Case<'a> {
name: &'a str,
pidfile_raw: Option<&'a str>, spawn_child: bool, expect_ok: bool, pf_removed: bool, argv_scan: bool, }
let cases = [
Case {
name: "no_match",
pidfile_raw: None,
spawn_child: false,
expect_ok: false,
pf_removed: false,
argv_scan: false,
},
Case {
name: "corrupt_pidfile",
pidfile_raw: Some("not-a-number"),
spawn_child: false,
expect_ok: false,
pf_removed: true,
argv_scan: false,
},
Case {
name: "stale_pidfile",
pidfile_raw: Some("999999"), spawn_child: false,
expect_ok: false,
pf_removed: true,
argv_scan: false,
},
Case {
name: "pidfile_happy",
pidfile_raw: None, spawn_child: true,
expect_ok: true,
pf_removed: true,
argv_scan: false,
},
Case {
name: "argv_scan",
pidfile_raw: None,
spawn_child: true,
expect_ok: true,
pf_removed: false,
argv_scan: true,
},
];
for Case {
name,
pidfile_raw,
spawn_child,
expect_ok,
pf_removed,
argv_scan,
} in cases
{
let td = tempdir().unwrap();
let host = name;
let pid_id = sanitize(format!("{TEST_EXE}_unix_{host}").to_ascii_lowercase());
let pidfile_path = td.path().join(format!("{pid_id}.pid"));
let child = if spawn_child {
#[cfg(unix)]
{
let mut c = std::process::Command::new("sh");
c.args(["-c", "sleep 30"]);
if argv_scan {
c.arg("--host").arg(host);
}
Some(c.spawn().unwrap())
}
#[cfg(windows)]
{
let mut c = std::process::Command::new("cmd");
c.args(["/C", "timeout", "/T", "30", "/NOBREAK"]);
if argv_scan {
c.arg("--host").arg(host);
}
Some(c.spawn().unwrap())
}
} else {
None
};
if let Some(contents) = pidfile_raw {
std::fs::write(&pidfile_path, contents.as_bytes()).unwrap();
} else if spawn_child && !argv_scan {
let pid = child.as_ref().unwrap().id();
std::fs::write(&pidfile_path, pid.to_string()).unwrap();
}
let result = kill_by_client(&pidfile_path, host);
if expect_ok {
result.unwrap();
} else {
matches!(
result.expect_err("should fail"),
ProcessError::NoSuchProcess { .. }
);
}
let expect_exists = pidfile_raw.is_some() && !pf_removed;
assert_eq!(
pidfile_path.exists(),
expect_exists,
"[{name}] pid-file existence mismatch (expected {expect_exists})"
);
if let Some(mut ch) = child {
let _ = ch.wait();
assert!(
!pid_alive(ch.id()).unwrap_or(true),
"[{name}] child process not killed"
);
}
}
}
}