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").args(["/PID", &pid_arg, "/T"]).status() {
390 Ok(status) if status.success() => {
391 std::thread::sleep(grace_period);
392 GracefulTerminationResult::GracefulExit
393 }
394 Ok(_) => match kill_process(pid) {
395 Ok(()) => GracefulTerminationResult::ForcefulKill,
396 Err(_) => GracefulTerminationResult::AlreadyExited,
397 },
398 Err(_) => GracefulTerminationResult::Error,
399 }
400 }
401 #[cfg(not(windows))]
402 {
403 let _ = (pid, initial_signal, grace_period);
404 GracefulTerminationResult::Error
405 }
406}
407
408pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
412 graceful_kill_process_group(
413 pid,
414 KillSignal::Term,
415 std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS),
416 )
417}
418
419pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
424 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
425 .await
426 .unwrap_or(GracefulTerminationResult::Error)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_set_parent_death_signal_no_panic() {
435 #[cfg(target_os = "linux")]
437 {
438 let parent_pid = unistd::getpid().as_raw();
439 let _ = set_parent_death_signal(parent_pid);
442 }
443 #[cfg(not(target_os = "linux"))]
444 {
445 assert!(set_parent_death_signal(0).is_ok());
446 }
447 }
448
449 #[test]
450 fn test_kill_nonexistent_process_group() {
451 #[cfg(unix)]
454 {
455 let result = kill_process_group(2_000_000_000);
458 let _ = result;
460 }
461 #[cfg(not(unix))]
462 {
463 let result = kill_process_group(999_999);
464 assert!(result.is_ok());
465 }
466 }
467
468 #[test]
469 fn test_kill_signal_values() {
470 assert_ne!(KillSignal::Int, KillSignal::Term);
472 assert_ne!(KillSignal::Term, KillSignal::Kill);
473 assert_ne!(KillSignal::Int, KillSignal::Kill);
474
475 assert_eq!(KillSignal::default(), KillSignal::Kill);
477 }
478
479 #[test]
480 fn test_graceful_termination_result_debug() {
481 let results = [
483 GracefulTerminationResult::GracefulExit,
484 GracefulTerminationResult::ForcefulKill,
485 GracefulTerminationResult::AlreadyExited,
486 GracefulTerminationResult::Error,
487 ];
488 for result in &results {
489 let _ = format!("{result:?}");
490 }
491 }
492
493 #[test]
494 fn test_graceful_kill_nonexistent_process() {
495 let result = graceful_kill_process_group_default(2_000_000_000);
497 #[cfg(unix)]
498 {
499 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
501 }
502 #[cfg(not(unix))]
503 {
504 let _ = result;
506 }
507 }
508
509 #[tokio::test]
510 async fn test_graceful_kill_nonexistent_process_async() {
511 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
512 #[cfg(unix)]
513 {
514 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
515 }
516 #[cfg(not(unix))]
517 {
518 let _ = result;
519 }
520 }
521
522 #[cfg(unix)]
523 #[test]
524 fn test_is_process_running_self() {
525 let pid = std::process::id();
527 assert!(is_process_running(pid));
528 }
529
530 #[cfg(unix)]
531 #[test]
532 fn test_is_process_running_nonexistent() {
533 assert!(!is_process_running(2_000_000_000));
535 }
536}