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 use std::io::ErrorKind;
205
206 let pgid = Pid::from_raw(process_group_id as libc::pid_t);
207 if let Err(err) = signal::killpg(pgid, signal.as_nix_signal()) {
208 let io_err = nix_err_to_io(err);
209 if io_err.kind() != ErrorKind::NotFound {
210 return Err(io_err);
211 }
212 }
213
214 Ok(())
215}
216
217#[cfg(windows)]
219pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
220 kill_process(process_group_id)
221}
222
223#[cfg(windows)]
225pub fn kill_process_group_with_signal(process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
226 kill_process(process_group_id)
227}
228
229#[cfg(all(not(unix), not(windows)))]
231pub fn kill_process_group(_process_group_id: u32) -> io::Result<()> {
232 Ok(())
233}
234
235#[cfg(all(not(unix), not(windows)))]
237pub fn kill_process_group_with_signal(_process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
238 Ok(())
239}
240
241#[cfg(unix)]
243pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
244 kill_child_process_group_with_signal(child, KillSignal::Kill)
245}
246
247#[cfg(unix)]
249pub fn kill_child_process_group_with_signal(child: &mut Child, signal: KillSignal) -> io::Result<()> {
250 if let Some(pid) = child.id() {
251 return kill_process_group_by_pid_with_signal(pid, signal);
252 }
253
254 Ok(())
255}
256
257#[cfg(not(unix))]
259pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
260 Ok(())
261}
262
263#[cfg(not(unix))]
265pub fn kill_child_process_group_with_signal(_child: &mut tokio::process::Child, _signal: KillSignal) -> io::Result<()> {
266 Ok(())
267}
268
269#[cfg(windows)]
271pub fn kill_process(pid: u32) -> io::Result<()> {
272 let status = std::process::Command::new("taskkill")
273 .args(["/PID", &pid.to_string(), "/T", "/F"])
274 .status()?;
275 if status.success() {
276 Ok(())
277 } else {
278 Err(io::Error::other("taskkill failed"))
279 }
280}
281
282#[cfg(not(windows))]
284pub fn kill_process(_pid: u32) -> io::Result<()> {
285 Ok(())
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum GracefulTerminationResult {
291 GracefulExit,
293 ForcefulKill,
295 AlreadyExited,
297 Error,
299}
300
301#[cfg(unix)]
303fn is_process_running(pid: u32) -> bool {
304 let target_pid = Pid::from_raw(pid as libc::pid_t);
305 match signal::kill(target_pid, None::<Signal>) {
306 Ok(()) => true,
307 Err(Errno::EPERM) => true,
309 Err(_) => false,
310 }
311}
312
313#[cfg(not(unix))]
314#[allow(dead_code, reason = "Intentional compatibility, platform, or test-only suppression.")]
315fn is_process_running(_pid: u32) -> bool {
316 true
318}
319
320#[cfg(unix)]
334pub fn graceful_kill_process_group(
335 pid: u32,
336 initial_signal: KillSignal,
337 grace_period: std::time::Duration,
338) -> GracefulTerminationResult {
339 if !is_process_running(pid) {
341 return GracefulTerminationResult::AlreadyExited;
342 }
343
344 let target_pid = Pid::from_raw(pid as libc::pid_t);
346 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
347 return GracefulTerminationResult::AlreadyExited;
349 };
350
351 let signal = match initial_signal {
353 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
355 };
356
357 if let Err(err) = signal::killpg(pgid, signal) {
358 if err != Errno::ESRCH {
359 return GracefulTerminationResult::Error;
360 }
361 return GracefulTerminationResult::AlreadyExited;
362 }
363
364 let deadline = std::time::Instant::now() + grace_period;
366 let poll_interval = std::time::Duration::from_millis(10);
367
368 while std::time::Instant::now() < deadline {
369 if !is_process_running(pid) {
370 return GracefulTerminationResult::GracefulExit;
371 }
372 std::thread::sleep(poll_interval);
373 }
374
375 let _ = signal::killpg(pgid, Signal::SIGKILL);
379 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
380 if err == Errno::ESRCH {
381 return GracefulTerminationResult::GracefulExit;
383 }
384 return GracefulTerminationResult::Error;
385 }
386
387 GracefulTerminationResult::ForcefulKill
388}
389
390#[cfg(not(unix))]
395pub fn graceful_kill_process_group(
396 pid: u32,
397 initial_signal: KillSignal,
398 grace_period: std::time::Duration,
399) -> GracefulTerminationResult {
400 #[cfg(windows)]
401 {
402 let _ = initial_signal;
403 let pid_arg = pid.to_string();
404 match std::process::Command::new("taskkill").args(["/PID", &pid_arg, "/T"]).status() {
405 Ok(status) if status.success() => {
406 std::thread::sleep(grace_period);
407 match kill_process(pid) {
408 Ok(()) => GracefulTerminationResult::ForcefulKill,
409 Err(_) => GracefulTerminationResult::AlreadyExited,
410 }
411 }
412 Ok(_) => match kill_process(pid) {
413 Ok(()) => GracefulTerminationResult::ForcefulKill,
414 Err(_) => GracefulTerminationResult::AlreadyExited,
415 },
416 Err(_) => GracefulTerminationResult::Error,
417 }
418 }
419 #[cfg(not(windows))]
420 {
421 let _ = (pid, initial_signal, grace_period);
422 GracefulTerminationResult::Error
423 }
424}
425
426pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
430 graceful_kill_process_group(pid, KillSignal::Term, std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS))
431}
432
433pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
438 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
439 .await
440 .unwrap_or(GracefulTerminationResult::Error)
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_set_parent_death_signal_no_panic() {
449 #[cfg(target_os = "linux")]
451 {
452 let parent_pid = unistd::getpid().as_raw();
453 let _ = set_parent_death_signal(parent_pid);
456 }
457 #[cfg(not(target_os = "linux"))]
458 {
459 assert!(set_parent_death_signal(0).is_ok());
460 }
461 }
462
463 #[test]
464 fn test_kill_nonexistent_process_group() {
465 #[cfg(unix)]
468 {
469 let result = kill_process_group(2_000_000_000);
472 let _ = result;
474 }
475 #[cfg(not(unix))]
476 {
477 let result = kill_process_group(999_999);
478 assert!(result.is_ok());
479 }
480 }
481
482 #[test]
483 fn test_kill_signal_values() {
484 assert_ne!(KillSignal::Int, KillSignal::Term);
486 assert_ne!(KillSignal::Term, KillSignal::Kill);
487 assert_ne!(KillSignal::Int, KillSignal::Kill);
488
489 assert_eq!(KillSignal::default(), KillSignal::Kill);
491 }
492
493 #[test]
494 fn test_graceful_termination_result_debug() {
495 let results = [
497 GracefulTerminationResult::GracefulExit,
498 GracefulTerminationResult::ForcefulKill,
499 GracefulTerminationResult::AlreadyExited,
500 GracefulTerminationResult::Error,
501 ];
502 for result in &results {
503 let _ = format!("{result:?}");
504 }
505 }
506
507 #[test]
508 fn test_graceful_kill_nonexistent_process() {
509 let result = graceful_kill_process_group_default(2_000_000_000);
511 #[cfg(unix)]
512 {
513 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
515 }
516 #[cfg(not(unix))]
517 {
518 let _ = result;
520 }
521 }
522
523 #[tokio::test]
524 async fn test_graceful_kill_nonexistent_process_async() {
525 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
526 #[cfg(unix)]
527 {
528 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
529 }
530 #[cfg(not(unix))]
531 {
532 let _ = result;
533 }
534 }
535
536 #[cfg(unix)]
537 #[test]
538 fn test_is_process_running_self() {
539 let pid = std::process::id();
541 assert!(is_process_running(pid));
542 }
543
544 #[cfg(unix)]
545 #[test]
546 fn test_is_process_running_nonexistent() {
547 assert!(!is_process_running(2_000_000_000));
549 }
550}