vtcode_bash_runner/
process_group.rs1use std::io;
23
24#[cfg(windows)]
25mod windows;
26
27#[cfg(unix)]
28use nix::errno::Errno;
29#[cfg(target_os = "linux")]
30use nix::sys::prctl;
31#[cfg(unix)]
32use nix::sys::signal::{self, Signal};
33#[cfg(unix)]
34use nix::unistd::{self, Pid};
35#[cfg(unix)]
36use tokio::process::Child;
37
38pub const DEFAULT_GRACEFUL_TIMEOUT_MS: u64 = 500;
40
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum KillSignal {
44 Int,
46 Term,
48 #[default]
50 Kill,
51}
52
53#[cfg(unix)]
54impl KillSignal {
55 fn as_nix_signal(self) -> Signal {
56 match self {
57 KillSignal::Int => Signal::SIGINT,
58 KillSignal::Term => Signal::SIGTERM,
59 KillSignal::Kill => Signal::SIGKILL,
60 }
61 }
62}
63
64#[cfg(unix)]
65#[cold]
66fn nix_err_to_io(err: Errno) -> io::Error {
67 io::Error::from_raw_os_error(err as i32)
68}
69
70#[cfg(target_os = "linux")]
75pub fn set_parent_death_signal(parent_pid: libc::pid_t) -> io::Result<()> {
76 prctl::set_pdeathsig(Some(Signal::SIGTERM)).map_err(nix_err_to_io)?;
77
78 if unistd::getppid() != Pid::from_raw(parent_pid) {
80 signal::kill(unistd::getpid(), Signal::SIGTERM).map_err(nix_err_to_io)?;
81 }
82
83 Ok(())
84}
85
86#[cfg(not(target_os = "linux"))]
88pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> {
89 Ok(())
90}
91
92#[cfg(unix)]
97pub fn detach_from_tty() -> io::Result<()> {
98 match unistd::setsid() {
99 Ok(_) => Ok(()),
100 Err(Errno::EPERM) => set_process_group(),
102 Err(err) => Err(nix_err_to_io(err)),
103 }
104}
105
106#[cfg(not(unix))]
108pub fn detach_from_tty() -> io::Result<()> {
109 Ok(())
110}
111
112#[cfg(unix)]
116pub fn set_process_group() -> io::Result<()> {
117 unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(nix_err_to_io)
118}
119
120#[cfg(not(unix))]
122pub fn set_process_group() -> io::Result<()> {
123 Ok(())
124}
125
126#[cfg(unix)]
130pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> {
131 kill_process_group_by_pid_with_signal(pid, KillSignal::Kill)
132}
133
134#[cfg(unix)]
136pub fn kill_process_group_by_pid_with_signal(pid: u32, signal: KillSignal) -> io::Result<()> {
137 use std::io::ErrorKind;
138
139 let target_pid = Pid::from_raw(pid as libc::pid_t);
140 let pgid = unistd::getpgid(Some(target_pid));
141 let mut pgid_err = None;
142
143 match pgid {
144 Ok(group) => {
145 if let Err(err) = signal::killpg(group, signal.as_nix_signal()) {
146 let io_err = nix_err_to_io(err);
147 if io_err.kind() != ErrorKind::NotFound {
148 pgid_err = Some(io_err);
149 }
150 }
151 }
152 Err(err) => pgid_err = Some(nix_err_to_io(err)),
153 }
154
155 if let Err(err) = signal::kill(target_pid, signal.as_nix_signal()) {
159 let io_err = nix_err_to_io(err);
160 if io_err.kind() == ErrorKind::NotFound {
161 return Ok(());
163 }
164 if let Some(pgid_error) = pgid_err {
166 return Err(pgid_error);
167 }
168 return Err(io_err);
169 }
170
171 Ok(())
172}
173
174#[cfg(windows)]
176pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> {
177 kill_process(pid)
178}
179
180#[cfg(windows)]
182pub fn kill_process_group_by_pid_with_signal(pid: u32, _signal: KillSignal) -> io::Result<()> {
183 kill_process(pid)
184}
185
186#[cfg(all(not(unix), not(windows)))]
188pub fn kill_process_group_by_pid(_pid: u32) -> io::Result<()> {
189 Ok(())
190}
191
192#[cfg(all(not(unix), not(windows)))]
194pub fn kill_process_group_by_pid_with_signal(_pid: u32, _signal: KillSignal) -> io::Result<()> {
195 Ok(())
196}
197
198#[cfg(unix)]
200pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
201 kill_process_group_with_signal(process_group_id, KillSignal::Kill)
202}
203
204#[cfg(unix)]
206pub fn kill_process_group_with_signal(process_group_id: u32, signal: KillSignal) -> io::Result<()> {
207 let pgid = Pid::from_raw(process_group_id as libc::pid_t);
208 if let Err(err) = signal::killpg(pgid, signal.as_nix_signal()) {
209 if err != Errno::ESRCH {
212 return Err(nix_err_to_io(err));
213 }
214 }
215
216 Ok(())
217}
218
219#[cfg(windows)]
221pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
222 kill_process(process_group_id)
223}
224
225#[cfg(windows)]
227pub fn kill_process_group_with_signal(process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
228 kill_process(process_group_id)
229}
230
231#[cfg(all(not(unix), not(windows)))]
233pub fn kill_process_group(_process_group_id: u32) -> io::Result<()> {
234 Ok(())
235}
236
237#[cfg(all(not(unix), not(windows)))]
239pub fn kill_process_group_with_signal(_process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
240 Ok(())
241}
242
243#[cfg(unix)]
245pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
246 kill_child_process_group_with_signal(child, KillSignal::Kill)
247}
248
249#[cfg(unix)]
251pub fn kill_child_process_group_with_signal(child: &mut Child, signal: KillSignal) -> io::Result<()> {
252 if let Some(pid) = child.id() {
253 return kill_process_group_by_pid_with_signal(pid, signal);
254 }
255
256 Ok(())
257}
258
259#[cfg(not(unix))]
261pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
262 Ok(())
263}
264
265#[cfg(not(unix))]
267pub fn kill_child_process_group_with_signal(_child: &mut tokio::process::Child, _signal: KillSignal) -> io::Result<()> {
268 Ok(())
269}
270
271#[cfg(windows)]
273pub fn kill_process(pid: u32) -> io::Result<()> {
274 windows::kill_process(pid)
275}
276
277#[cfg(not(windows))]
279pub fn kill_process(_pid: u32) -> io::Result<()> {
280 Ok(())
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum GracefulTerminationResult {
286 GracefulExit,
288 ForcefulKill,
290 AlreadyExited,
292 Error,
294}
295
296#[cfg(unix)]
298fn is_process_running(pid: u32) -> bool {
299 let target_pid = Pid::from_raw(pid as libc::pid_t);
300 match signal::kill(target_pid, None::<Signal>) {
301 Ok(()) => true,
302 Err(Errno::EPERM) => true,
304 Err(_) => false,
305 }
306}
307
308#[cfg(not(unix))]
309fn is_process_running(_pid: u32) -> bool {
310 true
312}
313
314#[cfg(unix)]
328pub fn graceful_kill_process_group(
329 pid: u32,
330 initial_signal: KillSignal,
331 grace_period: std::time::Duration,
332) -> GracefulTerminationResult {
333 if !is_process_running(pid) {
335 return GracefulTerminationResult::AlreadyExited;
336 }
337
338 let target_pid = Pid::from_raw(pid as libc::pid_t);
340 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
341 return GracefulTerminationResult::AlreadyExited;
343 };
344
345 let signal = match initial_signal {
347 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
349 };
350
351 if let Err(err) = signal::killpg(pgid, signal) {
352 if err != Errno::ESRCH {
353 return GracefulTerminationResult::Error;
354 }
355 return GracefulTerminationResult::AlreadyExited;
356 }
357
358 let deadline = std::time::Instant::now() + grace_period;
360 let poll_interval = std::time::Duration::from_millis(10);
361
362 while std::time::Instant::now() < deadline {
363 if !is_process_running(pid) {
364 return GracefulTerminationResult::GracefulExit;
365 }
366 std::thread::sleep(poll_interval);
367 }
368
369 let _ = signal::killpg(pgid, Signal::SIGKILL);
373 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
374 if err == Errno::ESRCH {
375 return GracefulTerminationResult::GracefulExit;
377 }
378 return GracefulTerminationResult::Error;
379 }
380
381 GracefulTerminationResult::ForcefulKill
382}
383
384#[cfg(not(unix))]
389pub fn graceful_kill_process_group(
390 pid: u32,
391 initial_signal: KillSignal,
392 grace_period: std::time::Duration,
393) -> GracefulTerminationResult {
394 #[cfg(windows)]
395 {
396 let _ = initial_signal;
397 let pid_arg = pid.to_string();
398 match std::process::Command::new("taskkill").args(["/PID", &pid_arg, "/T"]).output() {
399 Ok(output) if output.status.success() => {
400 std::thread::sleep(grace_period);
401 match kill_process(pid) {
402 Ok(()) => GracefulTerminationResult::ForcefulKill,
403 Err(_) => GracefulTerminationResult::AlreadyExited,
404 }
405 }
406 Ok(_) => match kill_process(pid) {
407 Ok(()) => GracefulTerminationResult::ForcefulKill,
408 Err(_) => GracefulTerminationResult::AlreadyExited,
409 },
410 Err(_) => GracefulTerminationResult::Error,
411 }
412 }
413 #[cfg(not(windows))]
414 {
415 let _ = (pid, initial_signal, grace_period);
416 GracefulTerminationResult::Error
417 }
418}
419
420pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
424 graceful_kill_process_group(pid, KillSignal::Term, std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS))
425}
426
427pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
432 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
433 .await
434 .unwrap_or(GracefulTerminationResult::Error)
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[cfg(unix)]
442 #[test]
443 fn process_group_cleanup_is_idempotent_after_child_exit() {
444 use std::os::unix::process::CommandExt;
445 let mut child = std::process::Command::new("/bin/sh")
446 .args(["-c", "exit 0"])
447 .process_group(0)
448 .spawn()
449 .expect("spawn isolated group");
450 let pid = child.id();
451 assert!(child.wait().expect("reap child").success());
452 assert!(kill_process_group(pid).is_ok());
453 assert!(kill_process_group_with_signal(pid, KillSignal::Int).is_ok());
454 }
455
456 #[test]
457 fn test_set_parent_death_signal_no_panic() {
458 #[cfg(target_os = "linux")]
460 {
461 let parent_pid = unistd::getpid().as_raw();
462 let _ = set_parent_death_signal(parent_pid);
465 }
466 #[cfg(not(target_os = "linux"))]
467 {
468 assert!(set_parent_death_signal(0).is_ok());
469 }
470 }
471
472 #[test]
473 fn test_kill_nonexistent_process_group() {
474 #[cfg(unix)]
477 {
478 let result = kill_process_group(2_000_000_000);
481 let _ = result;
483 }
484 #[cfg(not(unix))]
485 {
486 let result = kill_process_group(999_999);
487 assert!(result.is_ok());
488 }
489 }
490
491 #[test]
492 fn test_kill_signal_values() {
493 assert_ne!(KillSignal::Int, KillSignal::Term);
495 assert_ne!(KillSignal::Term, KillSignal::Kill);
496 assert_ne!(KillSignal::Int, KillSignal::Kill);
497
498 assert_eq!(KillSignal::default(), KillSignal::Kill);
500 }
501
502 #[test]
503 fn test_graceful_termination_result_debug() {
504 let results = [
506 GracefulTerminationResult::GracefulExit,
507 GracefulTerminationResult::ForcefulKill,
508 GracefulTerminationResult::AlreadyExited,
509 GracefulTerminationResult::Error,
510 ];
511 for result in &results {
512 let _ = format!("{result:?}");
513 }
514 }
515
516 #[test]
517 fn test_graceful_kill_nonexistent_process() {
518 let result = graceful_kill_process_group_default(2_000_000_000);
520 #[cfg(unix)]
521 {
522 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
524 }
525 #[cfg(not(unix))]
526 {
527 let _ = result;
529 }
530 }
531
532 #[tokio::test]
533 async fn test_graceful_kill_nonexistent_process_async() {
534 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
535 #[cfg(unix)]
536 {
537 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
538 }
539 #[cfg(not(unix))]
540 {
541 let _ = result;
542 }
543 }
544
545 #[cfg(unix)]
546 #[test]
547 fn test_is_process_running_self() {
548 let pid = std::process::id();
550 assert!(is_process_running(pid));
551 }
552
553 #[cfg(unix)]
554 #[test]
555 fn test_is_process_running_nonexistent() {
556 assert!(!is_process_running(2_000_000_000));
558 }
559}