vtcode_bash_runner/
process_group.rs1use 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(not(unix))]
173pub fn kill_process_group_by_pid(_pid: u32) -> io::Result<()> {
174 Ok(())
175}
176
177#[cfg(not(unix))]
179pub fn kill_process_group_by_pid_with_signal(_pid: u32, _signal: KillSignal) -> io::Result<()> {
180 Ok(())
181}
182
183#[cfg(unix)]
185pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
186 kill_process_group_with_signal(process_group_id, KillSignal::Kill)
187}
188
189#[cfg(unix)]
191pub fn kill_process_group_with_signal(process_group_id: u32, signal: KillSignal) -> io::Result<()> {
192 use std::io::ErrorKind;
193
194 let pgid = Pid::from_raw(process_group_id as libc::pid_t);
195 if let Err(err) = signal::killpg(pgid, signal.as_nix_signal()) {
196 let io_err = nix_err_to_io(err);
197 if io_err.kind() != ErrorKind::NotFound {
198 return Err(io_err);
199 }
200 }
201
202 Ok(())
203}
204
205#[cfg(not(unix))]
207pub fn kill_process_group(_process_group_id: u32) -> io::Result<()> {
208 Ok(())
209}
210
211#[cfg(not(unix))]
213pub fn kill_process_group_with_signal(
214 _process_group_id: u32,
215 _signal: KillSignal,
216) -> io::Result<()> {
217 Ok(())
218}
219
220#[cfg(unix)]
222pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
223 kill_child_process_group_with_signal(child, KillSignal::Kill)
224}
225
226#[cfg(unix)]
228pub fn kill_child_process_group_with_signal(
229 child: &mut Child,
230 signal: KillSignal,
231) -> io::Result<()> {
232 if let Some(pid) = child.id() {
233 return kill_process_group_by_pid_with_signal(pid, signal);
234 }
235
236 Ok(())
237}
238
239#[cfg(not(unix))]
241pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
242 Ok(())
243}
244
245#[cfg(not(unix))]
247pub fn kill_child_process_group_with_signal(
248 _child: &mut tokio::process::Child,
249 _signal: KillSignal,
250) -> io::Result<()> {
251 Ok(())
252}
253
254#[cfg(windows)]
256pub fn kill_process(pid: u32) -> io::Result<()> {
257 let status = std::process::Command::new("taskkill")
258 .args(["/PID", &pid.to_string(), "/T", "/F"])
259 .status()?;
260 if status.success() {
261 Ok(())
262 } else {
263 Err(io::Error::other("taskkill failed"))
264 }
265}
266
267#[cfg(not(windows))]
269pub fn kill_process(_pid: u32) -> io::Result<()> {
270 Ok(())
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum GracefulTerminationResult {
276 GracefulExit,
278 ForcefulKill,
280 AlreadyExited,
282 Error,
284}
285
286#[cfg(unix)]
288fn is_process_running(pid: u32) -> bool {
289 let target_pid = Pid::from_raw(pid as libc::pid_t);
290 match signal::kill(target_pid, None::<Signal>) {
291 Ok(()) => true,
292 Err(Errno::EPERM) => true,
294 Err(_) => false,
295 }
296}
297
298#[cfg(not(unix))]
299#[allow(dead_code)]
300fn is_process_running(_pid: u32) -> bool {
301 true
303}
304
305#[cfg(unix)]
319pub fn graceful_kill_process_group(
320 pid: u32,
321 initial_signal: KillSignal,
322 grace_period: std::time::Duration,
323) -> GracefulTerminationResult {
324 if !is_process_running(pid) {
326 return GracefulTerminationResult::AlreadyExited;
327 }
328
329 let target_pid = Pid::from_raw(pid as libc::pid_t);
331 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
332 return GracefulTerminationResult::AlreadyExited;
334 };
335
336 let signal = match initial_signal {
338 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
340 };
341
342 if let Err(err) = signal::killpg(pgid, signal) {
343 if err != Errno::ESRCH {
344 return GracefulTerminationResult::Error;
345 }
346 return GracefulTerminationResult::AlreadyExited;
347 }
348
349 let deadline = std::time::Instant::now() + grace_period;
351 let poll_interval = std::time::Duration::from_millis(10);
352
353 while std::time::Instant::now() < deadline {
354 if !is_process_running(pid) {
355 return GracefulTerminationResult::GracefulExit;
356 }
357 std::thread::sleep(poll_interval);
358 }
359
360 let _ = signal::killpg(pgid, Signal::SIGKILL);
364 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
365 if err == Errno::ESRCH {
366 return GracefulTerminationResult::GracefulExit;
368 }
369 return GracefulTerminationResult::Error;
370 }
371
372 GracefulTerminationResult::ForcefulKill
373}
374
375#[cfg(not(unix))]
380pub fn graceful_kill_process_group(
381 pid: u32,
382 initial_signal: KillSignal,
383 grace_period: std::time::Duration,
384) -> GracefulTerminationResult {
385 #[cfg(windows)]
386 {
387 let _ = initial_signal;
388 let pid_arg = pid.to_string();
389 match std::process::Command::new("taskkill")
390 .args(["/PID", &pid_arg, "/T"])
391 .status()
392 {
393 Ok(status) if status.success() => {
394 std::thread::sleep(grace_period);
395 GracefulTerminationResult::GracefulExit
396 }
397 Ok(_) => match kill_process(pid) {
398 Ok(()) => GracefulTerminationResult::ForcefulKill,
399 Err(_) => GracefulTerminationResult::AlreadyExited,
400 },
401 Err(_) => GracefulTerminationResult::Error,
402 }
403 }
404 #[cfg(not(windows))]
405 {
406 let _ = (pid, initial_signal, grace_period);
407 GracefulTerminationResult::Error
408 }
409}
410
411pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
415 graceful_kill_process_group(
416 pid,
417 KillSignal::Term,
418 std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS),
419 )
420}
421
422pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
427 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
428 .await
429 .unwrap_or(GracefulTerminationResult::Error)
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_set_parent_death_signal_no_panic() {
438 #[cfg(target_os = "linux")]
440 {
441 let parent_pid = unistd::getpid().as_raw();
442 let _ = set_parent_death_signal(parent_pid);
445 }
446 #[cfg(not(target_os = "linux"))]
447 {
448 assert!(set_parent_death_signal(0).is_ok());
449 }
450 }
451
452 #[test]
453 fn test_kill_nonexistent_process_group() {
454 #[cfg(unix)]
457 {
458 let result = kill_process_group(2_000_000_000);
461 let _ = result;
463 }
464 #[cfg(not(unix))]
465 {
466 let result = kill_process_group(999_999);
467 assert!(result.is_ok());
468 }
469 }
470
471 #[test]
472 fn test_kill_signal_values() {
473 assert_ne!(KillSignal::Int, KillSignal::Term);
475 assert_ne!(KillSignal::Term, KillSignal::Kill);
476 assert_ne!(KillSignal::Int, KillSignal::Kill);
477
478 assert_eq!(KillSignal::default(), KillSignal::Kill);
480 }
481
482 #[test]
483 fn test_graceful_termination_result_debug() {
484 let results = [
486 GracefulTerminationResult::GracefulExit,
487 GracefulTerminationResult::ForcefulKill,
488 GracefulTerminationResult::AlreadyExited,
489 GracefulTerminationResult::Error,
490 ];
491 for result in &results {
492 let _ = format!("{result:?}");
493 }
494 }
495
496 #[test]
497 fn test_graceful_kill_nonexistent_process() {
498 let result = graceful_kill_process_group_default(2_000_000_000);
500 #[cfg(unix)]
501 {
502 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
504 }
505 #[cfg(not(unix))]
506 {
507 let _ = result;
509 }
510 }
511
512 #[tokio::test]
513 async fn test_graceful_kill_nonexistent_process_async() {
514 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
515 #[cfg(unix)]
516 {
517 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
518 }
519 #[cfg(not(unix))]
520 {
521 let _ = result;
522 }
523 }
524
525 #[cfg(unix)]
526 #[test]
527 fn test_is_process_running_self() {
528 let pid = std::process::id();
530 assert!(is_process_running(pid));
531 }
532
533 #[cfg(unix)]
534 #[test]
535 fn test_is_process_running_nonexistent() {
536 assert!(!is_process_running(2_000_000_000));
538 }
539}