1use std::io;
23
24#[cfg(unix)]
25use nix::errno::Errno;
26#[cfg(target_os = "linux")]
27use nix::sys::prctl;
28#[cfg(unix)]
29use nix::sys::signal::{self, Signal};
30#[cfg(unix)]
31use nix::unistd::{self, Pid};
32#[cfg(unix)]
33use tokio::process::Child;
34
35pub const DEFAULT_GRACEFUL_TIMEOUT_MS: u64 = 500;
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub enum KillSignal {
41 Int,
43 Term,
45 #[default]
47 Kill,
48}
49
50#[cfg(unix)]
51impl KillSignal {
52 fn as_nix_signal(self) -> Signal {
53 match self {
54 KillSignal::Int => Signal::SIGINT,
55 KillSignal::Term => Signal::SIGTERM,
56 KillSignal::Kill => Signal::SIGKILL,
57 }
58 }
59}
60
61#[cfg(unix)]
62#[cold]
63fn nix_err_to_io(err: Errno) -> io::Error {
64 io::Error::from_raw_os_error(err as i32)
65}
66
67#[cfg(target_os = "linux")]
72pub fn set_parent_death_signal(parent_pid: libc::pid_t) -> io::Result<()> {
73 prctl::set_pdeathsig(Some(Signal::SIGTERM)).map_err(nix_err_to_io)?;
74
75 if unistd::getppid() != Pid::from_raw(parent_pid) {
77 signal::kill(unistd::getpid(), Signal::SIGTERM).map_err(nix_err_to_io)?;
78 }
79
80 Ok(())
81}
82
83#[cfg(not(target_os = "linux"))]
85pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> {
86 Ok(())
87}
88
89#[cfg(unix)]
94pub fn detach_from_tty() -> io::Result<()> {
95 match unistd::setsid() {
96 Ok(_) => Ok(()),
97 Err(Errno::EPERM) => set_process_group(),
99 Err(err) => Err(nix_err_to_io(err)),
100 }
101}
102
103#[cfg(not(unix))]
105pub fn detach_from_tty() -> io::Result<()> {
106 Ok(())
107}
108
109#[cfg(unix)]
113pub fn set_process_group() -> io::Result<()> {
114 unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(nix_err_to_io)
115}
116
117#[cfg(not(unix))]
119pub fn set_process_group() -> io::Result<()> {
120 Ok(())
121}
122
123#[cfg(unix)]
127pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> {
128 kill_process_group_by_pid_with_signal(pid, KillSignal::Kill)
129}
130
131#[cfg(unix)]
133pub fn kill_process_group_by_pid_with_signal(pid: u32, signal: KillSignal) -> io::Result<()> {
134 use std::io::ErrorKind;
135
136 let target_pid = Pid::from_raw(pid as libc::pid_t);
137 let pgid = unistd::getpgid(Some(target_pid));
138 let mut pgid_err = None;
139
140 match pgid {
141 Ok(group) => {
142 if let Err(err) = signal::killpg(group, signal.as_nix_signal()) {
143 let io_err = nix_err_to_io(err);
144 if io_err.kind() != ErrorKind::NotFound {
145 pgid_err = Some(io_err);
146 }
147 }
148 }
149 Err(err) => pgid_err = Some(nix_err_to_io(err)),
150 }
151
152 if let Err(err) = signal::kill(target_pid, signal.as_nix_signal()) {
156 let io_err = nix_err_to_io(err);
157 if io_err.kind() == ErrorKind::NotFound {
158 return Ok(());
160 }
161 if let Some(pgid_error) = pgid_err {
163 return Err(pgid_error);
164 }
165 return Err(io_err);
166 }
167
168 Ok(())
169}
170
171#[cfg(windows)]
173pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> {
174 kill_process(pid)
175}
176
177#[cfg(windows)]
179pub fn kill_process_group_by_pid_with_signal(pid: u32, _signal: KillSignal) -> io::Result<()> {
180 kill_process(pid)
181}
182
183#[cfg(all(not(unix), not(windows)))]
185pub fn kill_process_group_by_pid(_pid: u32) -> io::Result<()> {
186 Ok(())
187}
188
189#[cfg(all(not(unix), not(windows)))]
191pub fn kill_process_group_by_pid_with_signal(_pid: u32, _signal: KillSignal) -> io::Result<()> {
192 Ok(())
193}
194
195#[cfg(unix)]
197pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
198 kill_process_group_with_signal(process_group_id, KillSignal::Kill)
199}
200
201#[cfg(unix)]
203pub fn kill_process_group_with_signal(process_group_id: u32, signal: KillSignal) -> io::Result<()> {
204 let pgid = Pid::from_raw(process_group_id as libc::pid_t);
205 if let Err(err) = signal::killpg(pgid, signal.as_nix_signal()) {
206 if err != Errno::ESRCH {
209 return Err(nix_err_to_io(err));
210 }
211 }
212
213 Ok(())
214}
215
216#[cfg(windows)]
218pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
219 kill_process(process_group_id)
220}
221
222#[cfg(windows)]
224pub fn kill_process_group_with_signal(process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
225 kill_process(process_group_id)
226}
227
228#[cfg(all(not(unix), not(windows)))]
230pub fn kill_process_group(_process_group_id: u32) -> io::Result<()> {
231 Ok(())
232}
233
234#[cfg(all(not(unix), not(windows)))]
236pub fn kill_process_group_with_signal(_process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
237 Ok(())
238}
239
240#[cfg(unix)]
242pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
243 kill_child_process_group_with_signal(child, KillSignal::Kill)
244}
245
246#[cfg(unix)]
248pub fn kill_child_process_group_with_signal(child: &mut Child, signal: KillSignal) -> io::Result<()> {
249 if let Some(pid) = child.id() {
250 return kill_process_group_by_pid_with_signal(pid, signal);
251 }
252
253 Ok(())
254}
255
256#[cfg(not(unix))]
258pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
259 Ok(())
260}
261
262#[cfg(not(unix))]
264pub fn kill_child_process_group_with_signal(_child: &mut tokio::process::Child, _signal: KillSignal) -> io::Result<()> {
265 Ok(())
266}
267
268#[cfg(windows)]
270pub fn kill_process(pid: u32) -> io::Result<()> {
271 let status = std::process::Command::new("taskkill")
272 .args(["/PID", &pid.to_string(), "/T", "/F"])
273 .status()?;
274 if status.success() {
275 Ok(())
276 } else {
277 Err(io::Error::other("taskkill failed"))
278 }
279}
280
281#[cfg(not(windows))]
283pub fn kill_process(_pid: u32) -> io::Result<()> {
284 Ok(())
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum GracefulTerminationResult {
290 GracefulExit,
292 ForcefulKill,
294 AlreadyExited,
296 Error,
298}
299
300#[cfg(unix)]
302fn is_process_running(pid: u32) -> bool {
303 let target_pid = Pid::from_raw(pid as libc::pid_t);
304 match signal::kill(target_pid, None::<Signal>) {
305 Ok(()) => true,
306 Err(Errno::EPERM) => true,
308 Err(_) => false,
309 }
310}
311
312#[cfg(not(unix))]
313#[allow(dead_code, reason = "Intentional compatibility, platform, or test-only suppression.")]
314fn is_process_running(_pid: u32) -> bool {
315 true
317}
318
319#[cfg(unix)]
333pub fn graceful_kill_process_group(
334 pid: u32,
335 initial_signal: KillSignal,
336 grace_period: std::time::Duration,
337) -> GracefulTerminationResult {
338 if !is_process_running(pid) {
340 return GracefulTerminationResult::AlreadyExited;
341 }
342
343 let target_pid = Pid::from_raw(pid as libc::pid_t);
345 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
346 return GracefulTerminationResult::AlreadyExited;
348 };
349
350 let signal = match initial_signal {
352 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
354 };
355
356 if let Err(err) = signal::killpg(pgid, signal) {
357 if err != Errno::ESRCH {
358 return GracefulTerminationResult::Error;
359 }
360 return GracefulTerminationResult::AlreadyExited;
361 }
362
363 let deadline = std::time::Instant::now() + grace_period;
365 let poll_interval = std::time::Duration::from_millis(10);
366
367 while std::time::Instant::now() < deadline {
368 if !is_process_running(pid) {
369 return GracefulTerminationResult::GracefulExit;
370 }
371 std::thread::sleep(poll_interval);
372 }
373
374 let _ = signal::killpg(pgid, Signal::SIGKILL);
378 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
379 if err == Errno::ESRCH {
380 return GracefulTerminationResult::GracefulExit;
382 }
383 return GracefulTerminationResult::Error;
384 }
385
386 GracefulTerminationResult::ForcefulKill
387}
388
389#[cfg(not(unix))]
394pub fn graceful_kill_process_group(
395 pid: u32,
396 initial_signal: KillSignal,
397 grace_period: std::time::Duration,
398) -> GracefulTerminationResult {
399 #[cfg(windows)]
400 {
401 let _ = initial_signal;
402 let pid_arg = pid.to_string();
403 match std::process::Command::new("taskkill").args(["/PID", &pid_arg, "/T"]).status() {
404 Ok(status) if status.success() => {
405 std::thread::sleep(grace_period);
406 match kill_process(pid) {
407 Ok(()) => GracefulTerminationResult::ForcefulKill,
408 Err(_) => GracefulTerminationResult::AlreadyExited,
409 }
410 }
411 Ok(_) => match kill_process(pid) {
412 Ok(()) => GracefulTerminationResult::ForcefulKill,
413 Err(_) => GracefulTerminationResult::AlreadyExited,
414 },
415 Err(_) => GracefulTerminationResult::Error,
416 }
417 }
418 #[cfg(not(windows))]
419 {
420 let _ = (pid, initial_signal, grace_period);
421 GracefulTerminationResult::Error
422 }
423}
424
425pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
429 graceful_kill_process_group(pid, KillSignal::Term, std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS))
430}
431
432pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
437 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
438 .await
439 .unwrap_or(GracefulTerminationResult::Error)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[cfg(unix)]
447 #[test]
448 fn process_group_cleanup_is_idempotent_after_child_exit() {
449 use std::os::unix::process::CommandExt;
450 let mut child = std::process::Command::new("/bin/sh")
451 .args(["-c", "exit 0"])
452 .process_group(0)
453 .spawn()
454 .expect("spawn isolated group");
455 let pid = child.id();
456 assert!(child.wait().expect("reap child").success());
457 assert!(kill_process_group(pid).is_ok());
458 assert!(kill_process_group_with_signal(pid, KillSignal::Int).is_ok());
459 }
460
461 #[test]
462 fn test_set_parent_death_signal_no_panic() {
463 #[cfg(target_os = "linux")]
465 {
466 let parent_pid = unistd::getpid().as_raw();
467 let _ = set_parent_death_signal(parent_pid);
470 }
471 #[cfg(not(target_os = "linux"))]
472 {
473 assert!(set_parent_death_signal(0).is_ok());
474 }
475 }
476
477 #[test]
478 fn test_kill_nonexistent_process_group() {
479 #[cfg(unix)]
482 {
483 let result = kill_process_group(2_000_000_000);
486 let _ = result;
488 }
489 #[cfg(not(unix))]
490 {
491 let result = kill_process_group(999_999);
492 assert!(result.is_ok());
493 }
494 }
495
496 #[test]
497 fn test_kill_signal_values() {
498 assert_ne!(KillSignal::Int, KillSignal::Term);
500 assert_ne!(KillSignal::Term, KillSignal::Kill);
501 assert_ne!(KillSignal::Int, KillSignal::Kill);
502
503 assert_eq!(KillSignal::default(), KillSignal::Kill);
505 }
506
507 #[test]
508 fn test_graceful_termination_result_debug() {
509 let results = [
511 GracefulTerminationResult::GracefulExit,
512 GracefulTerminationResult::ForcefulKill,
513 GracefulTerminationResult::AlreadyExited,
514 GracefulTerminationResult::Error,
515 ];
516 for result in &results {
517 let _ = format!("{result:?}");
518 }
519 }
520
521 #[test]
522 fn test_graceful_kill_nonexistent_process() {
523 let result = graceful_kill_process_group_default(2_000_000_000);
525 #[cfg(unix)]
526 {
527 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
529 }
530 #[cfg(not(unix))]
531 {
532 let _ = result;
534 }
535 }
536
537 #[tokio::test]
538 async fn test_graceful_kill_nonexistent_process_async() {
539 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
540 #[cfg(unix)]
541 {
542 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
543 }
544 #[cfg(not(unix))]
545 {
546 let _ = result;
547 }
548 }
549
550 #[cfg(unix)]
551 #[test]
552 fn test_is_process_running_self() {
553 let pid = std::process::id();
555 assert!(is_process_running(pid));
556 }
557
558 #[cfg(unix)]
559 #[test]
560 fn test_is_process_running_nonexistent() {
561 assert!(!is_process_running(2_000_000_000));
563 }
564}