1use core::str;
49use std::{
50 path::Path,
51 time::{Duration, Instant},
52};
53
54use super::{error::*, pid::*, *};
55
56pub fn kill_by_client(pidfile_path: &Path, host: &str) -> Result<()> {
57 let pid: Option<u32> = std::fs::read_to_string(pidfile_path)
59 .ok()
60 .and_then(|s| s.trim().parse().ok());
61 if let Some(pid) = pid {
62 match pid_alive(pid) {
63 Ok(true) => {
65 crate::info!("Killing server at {host} (PID {pid}) via pid-file");
66 let res = kill_pids(&[pid], POLITE_WAIT);
67
68 if res.is_ok() {
69 if let Err(e) = std::fs::remove_file(pidfile_path) {
70 crate::warn!("Failed to remove pid-file for host {host}: {e}");
71 }
72 }
73 return res;
74 }
75 Ok(false) => match std::fs::remove_file(pidfile_path) {
77 Ok(_) => (),
78 Err(e) => crate::warn!("Failed to remove stale pid-file for host {host}: {e}"),
79 },
80 Err(e) => {
82 crate::warn!("pid_alive({pid}) failed: {e}. Falling back to argv scan…");
83 }
84 }
85 } else {
86 if let Err(e) = std::fs::remove_file(pidfile_path) {
87 crate::warn!("Failed to remove malformed pid-file for host {host}: {e}");
88 }
89 };
90
91 let patterns: &[&[&str]] = &[&["--host", host], &["-h", host]];
93 if let Some(pid) = get_server_pid_by_cmd_args(patterns) {
94 crate::info!("Killing server at {host} (PID {pid}) via argv scan");
95 match kill_pids(&[pid], POLITE_WAIT) {
96 Ok(()) => {
97 match std::fs::remove_file(pidfile_path) {
98 Ok(_) => (),
99 Err(e) => {
100 crate::warn!("Failed to remove stale pid-file for host {host}: {e}")
101 }
102 }
103 return Ok(());
104 }
105 Err(e) => {
106 crate::warn!("Failed to kill server at {host} (PID {pid}): {e}");
107 }
108 }
109 }
110
111 Err(ProcessError::NoSuchProcess {
113 query: format!("host={host}"),
114 })
115}
116
117pub fn kill_all_servers(executable_name: &str) -> Result<()> {
118 crate::info!("Killing all {executable_name} processes");
119
120 let pids = get_all_server_pids(executable_name);
122 let mut errors = Vec::new();
123
124 if !pids.is_empty() {
125 if let Err(e) = kill_pids(&pids, POLITE_WAIT) {
126 errors.push(e);
127 }
128 }
129
130 for pid in pids {
132 match pid_alive(pid) {
134 Ok(false) => crate::info!("PID {pid} shut down"),
135 Ok(true) => crate::warn!("PID {pid} still alive, but we tried to kill it"),
136 Err(e) => crate::warn!("Could not probe PID {pid}: {e}"),
137 }
138 }
139
140 if errors.is_empty() {
142 Ok(())
143 } else {
144 for e in &errors[1..] {
146 crate::warn!("Additional error while killing servers: {e}");
147 }
148 Err(errors.remove(0))
149 }
150}
151
152fn kill_pids(pids: &[u32], polite_wait: Duration) -> Result<()> {
153 let mut seen = std::collections::HashSet::with_capacity(pids.len());
154 let uniq: Vec<u32> = pids.iter().copied().filter(|p| seen.insert(*p)).collect();
155 if uniq.is_empty() {
156 return Ok(());
157 }
158 let start = Instant::now();
159 for pid in &uniq {
161 match pid_alive(*pid) {
162 Ok(true) => match kill_pid(*pid) {
163 Ok(()) => crate::info!("Sent TERM to PID {}", pid),
164 Err(e) => crate::error!("Failed to send TERM to PID {}: {}", pid, e),
165 },
166 Ok(false) => (),
167 Err(e) => crate::error!("Failed to check PID {}: {}", pid, e),
168 }
169 }
170
171 let polite_deadline = Instant::now() + polite_wait;
173 let mut probe_failures: Vec<(u32, ProcessError)> = Vec::new();
174
175 while Instant::now() < polite_deadline {
176 let all_dead = pids.iter().all(|&pid| match pid_alive(pid) {
177 Ok(alive) => !alive,
178 Err(e) => {
179 if !probe_failures.iter().any(|(p, _)| *p == pid) {
181 probe_failures.push((pid, e));
182 }
183 false
184 }
185 });
186
187 if all_dead {
188 break;
189 }
190 std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
191 }
192
193 for &pid in pids {
195 if let Err(e) = force_kill_pid(pid) {
196 crate::error!("Failed to force-kill PID {pid}: {e}");
197 }
198 }
199
200 if !probe_failures.is_empty() {
201 for (pid, err) in &probe_failures {
202 crate::warn!("Never obtained status for PID {pid}: {err}");
203 }
204 }
205 let force_kill_deadline = Instant::now() + Duration::from_secs(FORCE_KILL_TIMEOUT_SECS);
206 while Instant::now() < force_kill_deadline {
207 if pids
208 .iter()
209 .all(|&pid| matches!(pid_alive(pid), Ok(false) | Err(_)))
210 {
211 break;
212 }
213 std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
214 }
215
216 #[cfg(target_os = "macos")]
217 for &pid in &uniq {
218 use nix::sys::wait::{waitpid, WaitPidFlag};
219 let _ = nix::unistd::Pid::from_raw(pid as i32);
220 let _ = waitpid(
222 nix::unistd::Pid::from_raw(pid as i32),
223 Some(WaitPidFlag::WNOHANG),
224 );
225 }
226
227 let leftovers: Vec<u32> = pids
229 .iter()
230 .copied()
231 .filter(|&pid| match pid_alive(pid) {
232 Ok(alive) => alive,
233 Err(_) => true, })
235 .collect();
236
237 let elapsed = start.elapsed();
238 if leftovers.is_empty() {
239 Ok(())
240 } else {
241 Err(ProcessError::TerminationTimeout {
242 operation: "kill_pids",
243 elapsed,
244 leftovers,
245 })
246 }
247}
248
249#[cfg(unix)]
250pub fn kill_pid(pid: u32) -> Result<()> {
251 use nix::{
252 errno::Errno,
253 sys::signal::{kill, Signal},
254 unistd::Pid,
255 };
256 match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
257 Ok(_) | Err(Errno::ESRCH) => Ok(()), Err(Errno::EPERM) => Err(ProcessError::PermissionDenied {
260 action: "send SIGTERM",
261 source: "operation not permitted".into(),
262 }),
263
264 Err(e) => Err(ProcessError::CommandFailed {
265 action: "send SIGTERM",
266 source: e.into(),
267 }),
268 }
269}
270
271#[cfg(unix)]
272fn force_kill_pid(pid: u32) -> Result<()> {
273 use nix::{
274 errno::Errno,
275 sys::signal::{kill, Signal},
276 unistd::Pid,
277 };
278 match kill(Pid::from_raw(pid as i32), Signal::SIGKILL) {
279 Ok(_) | Err(Errno::ESRCH) => Ok(()),
280
281 Err(Errno::EPERM) => Err(ProcessError::PermissionDenied {
282 action: "send SIGKILL",
283 source: "operation not permitted".into(),
284 }),
285
286 Err(e) => Err(ProcessError::CommandFailed {
287 action: "send SIGKILL",
288 source: e.into(),
289 }),
290 }
291}
292
293#[cfg(windows)]
294pub fn kill_pid(pid: u32) -> Result<()> {
295 use windows::Win32::{
296 Foundation::CloseHandle,
297 System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE},
298 };
299
300 unsafe {
301 let handle = OpenProcess(PROCESS_TERMINATE, false, pid).map_err(|e| {
303 ProcessError::CommandFailed {
304 action: "OpenProcess",
305 source: Box::new(e),
306 }
307 })?;
308
309 if handle.is_invalid() {
310 return Ok(());
312 }
313
314 let result = TerminateProcess(handle, 1);
316 let _ = CloseHandle(handle);
317
318 result.map_err(|e| ProcessError::CommandFailed {
319 action: "TerminateProcess",
320 source: Box::new(e),
321 })
322 }
323}
324
325#[cfg(windows)]
326pub fn force_kill_pid(pid: u32) -> Result<()> {
327 use windows::Win32::{
328 Foundation::CloseHandle,
329 System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE},
330 };
331 fn win32_error(action: &'static str) -> ProcessError {
332 let err = windows::core::Error::from_win32();
334 match err.code().0 {
335 5 => ProcessError::PermissionDenied {
336 action,
338 source: Box::new(err),
339 },
340 _ => ProcessError::CommandFailed {
341 action,
342 source: Box::new(err),
343 },
344 }
345 }
346
347 unsafe {
348 let h = OpenProcess(PROCESS_TERMINATE, false, pid).map_err(|e| {
349 ProcessError::CommandFailed {
350 action: "force-kill (OpenProcess)",
351 source: e.into(),
352 }
353 })?;
354 if h.is_invalid() {
355 let err = windows::core::Error::from_win32();
356 return match err.code().0 {
357 87 => Ok(()), _ => Err(win32_error("force-kill (OpenProcess)")),
359 };
360 }
361 match TerminateProcess(h, 1) {
362 Ok(_) => CloseHandle(h).map_err(|e| ProcessError::CommandFailed {
363 action: "force-kill (CloseHandle)",
364 source: e.into(),
365 }),
366 Err(_) => {
367 let _ = CloseHandle(h);
368 Err(win32_error("force-kill (TerminateProcess)"))
369 }
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use std::time::Duration;
377
378 use serial_test::serial;
379 use tempfile::tempdir;
380
381 use super::*;
382 use crate::server::process::tests_helpers::*;
383
384 #[test]
388 #[serial]
389 fn kill_pids_scenarios() {
390 use ProcessError::*;
391
392 assert!(kill_pids(&[], Duration::from_millis(10)).is_ok());
394
395 let dead_pid = {
398 let mut child = short_cmd().spawn().unwrap();
399 let pid = child.id();
400 let _ = child.wait();
401 pid
402 };
403 match kill_pids(&[dead_pid], Duration::from_millis(200)) {
404 Ok(()) | Err(TerminationTimeout { .. }) => {} Err(e) => panic!("unexpected error on all-dead slice: {e:?}"),
406 }
407
408 fn spawn_and_kill(wait: Duration, duplicate: bool) {
410 let mut child = long_cmd().spawn().unwrap();
411 let pid = child.id();
412 let pids = if duplicate { vec![pid, pid] } else { vec![pid] };
413
414 match kill_pids(&pids, wait) {
416 Ok(()) | Err(TerminationTimeout { .. }) => {}
417 Err(e) => panic!("kill_pids failed unexpectedly: {e:?}"),
418 }
419
420 let _ = child.wait();
421 assert!(
422 !pid_alive(pid).unwrap_or(true),
423 "child {pid} still alive after kill_pids(wait={wait:?}, dup={duplicate})"
424 );
425 }
426
427 spawn_and_kill(Duration::from_secs(2), true);
429
430 for &d in &[Duration::from_secs(2), Duration::from_secs(0)] {
432 spawn_and_kill(d, false);
433 }
434
435 let mut child_live = long_cmd().spawn().unwrap();
437 let pid_live = child_live.id();
438
439 let mut child_dead = long_cmd().spawn().unwrap();
440 let pid_dead = child_dead.id();
441 kill_pid(pid_dead).unwrap(); let _ = child_dead.wait(); match kill_pids(&[pid_dead, pid_live], Duration::from_millis(500)) {
445 Ok(()) | Err(TerminationTimeout { .. }) => {}
446 Err(e) => panic!("mixed kill failed unexpectedly: {e:?}"),
447 }
448 let _ = child_live.wait();
449 assert!(
450 !pid_alive(pid_live).unwrap_or(true),
451 "live child {pid_live} survived mixed-status kill"
452 );
453 }
454
455 #[test]
459 #[serial]
460 fn kill_by_client_scenarios() {
461 use sanitize_filename::sanitize; struct Case<'a> {
464 name: &'a str,
465 pidfile_raw: Option<&'a str>, spawn_child: bool, expect_ok: bool, pf_removed: bool, argv_scan: bool, }
471
472 let cases = [
473 Case {
474 name: "no_match",
475 pidfile_raw: None,
476 spawn_child: false,
477 expect_ok: false,
478 pf_removed: false,
479 argv_scan: false,
480 },
481 Case {
482 name: "corrupt_pidfile",
483 pidfile_raw: Some("not-a-number"),
484 spawn_child: false,
485 expect_ok: false,
486 pf_removed: true,
487 argv_scan: false,
488 },
489 Case {
490 name: "stale_pidfile",
491 pidfile_raw: Some("999999"), spawn_child: false,
493 expect_ok: false,
494 pf_removed: true,
495 argv_scan: false,
496 },
497 Case {
498 name: "pidfile_happy",
499 pidfile_raw: None, spawn_child: true,
501 expect_ok: true,
502 pf_removed: true,
503 argv_scan: false,
504 },
505 Case {
506 name: "argv_scan",
507 pidfile_raw: None,
508 spawn_child: true,
509 expect_ok: true,
510 pf_removed: false,
511 argv_scan: true,
512 },
513 ];
514
515 for Case {
516 name,
517 pidfile_raw,
518 spawn_child,
519 expect_ok,
520 pf_removed,
521 argv_scan,
522 } in cases
523 {
524 let td = tempdir().unwrap();
525 let host = name; let pid_id = sanitize(format!("{TEST_EXE}_unix_{host}").to_ascii_lowercase());
531 let pidfile_path = td.path().join(format!("{pid_id}.pid"));
532
533 let child = if spawn_child {
535 #[cfg(unix)]
536 {
537 let mut c = std::process::Command::new("sh");
538 c.args(["-c", "sleep 30"]);
539 if argv_scan {
540 c.arg("--host").arg(host);
541 }
542 Some(c.spawn().unwrap())
543 }
544 #[cfg(windows)]
545 {
546 let mut c = std::process::Command::new("cmd");
547 c.args(["/C", "timeout", "/T", "30", "/NOBREAK"]);
548 if argv_scan {
549 c.arg("--host").arg(host);
550 }
551 Some(c.spawn().unwrap())
552 }
553 } else {
554 None
555 };
556
557 if let Some(contents) = pidfile_raw {
559 std::fs::write(&pidfile_path, contents.as_bytes()).unwrap();
560 } else if spawn_child && !argv_scan {
561 let pid = child.as_ref().unwrap().id();
563 std::fs::write(&pidfile_path, pid.to_string()).unwrap();
564 }
565
566 let result = kill_by_client(&pidfile_path, host);
568
569 if expect_ok {
571 result.unwrap();
572 } else {
573 matches!(
574 result.expect_err("should fail"),
575 ProcessError::NoSuchProcess { .. }
576 );
577 }
578
579 let expect_exists = pidfile_raw.is_some() && !pf_removed;
581 assert_eq!(
582 pidfile_path.exists(),
583 expect_exists,
584 "[{name}] pid-file existence mismatch (expected {expect_exists})"
585 );
586
587 if let Some(mut ch) = child {
588 let _ = ch.wait();
589 assert!(
590 !pid_alive(ch.id()).unwrap_or(true),
591 "[{name}] child process not killed"
592 );
593 }
594 }
595 }
596}