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(_process_group_id: u32, _signal: KillSignal) -> io::Result<()> {
214 Ok(())
215}
216
217#[cfg(unix)]
219pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
220 kill_child_process_group_with_signal(child, KillSignal::Kill)
221}
222
223#[cfg(unix)]
225pub fn kill_child_process_group_with_signal(child: &mut Child, signal: KillSignal) -> io::Result<()> {
226 if let Some(pid) = child.id() {
227 return kill_process_group_by_pid_with_signal(pid, signal);
228 }
229
230 Ok(())
231}
232
233#[cfg(not(unix))]
235pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
236 Ok(())
237}
238
239#[cfg(not(unix))]
241pub fn kill_child_process_group_with_signal(_child: &mut tokio::process::Child, _signal: KillSignal) -> io::Result<()> {
242 Ok(())
243}
244
245#[cfg(windows)]
247pub fn kill_process(pid: u32) -> io::Result<()> {
248 let status = std::process::Command::new("taskkill")
249 .args(["/PID", &pid.to_string(), "/T", "/F"])
250 .status()?;
251 if status.success() {
252 Ok(())
253 } else {
254 Err(io::Error::other("taskkill failed"))
255 }
256}
257
258#[cfg(not(windows))]
260pub fn kill_process(_pid: u32) -> io::Result<()> {
261 Ok(())
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum GracefulTerminationResult {
267 GracefulExit,
269 ForcefulKill,
271 AlreadyExited,
273 Error,
275}
276
277#[cfg(unix)]
279fn is_process_running(pid: u32) -> bool {
280 let target_pid = Pid::from_raw(pid as libc::pid_t);
281 match signal::kill(target_pid, None::<Signal>) {
282 Ok(()) => true,
283 Err(Errno::EPERM) => true,
285 Err(_) => false,
286 }
287}
288
289#[cfg(not(unix))]
290#[allow(dead_code)]
291fn is_process_running(_pid: u32) -> bool {
292 true
294}
295
296#[cfg(unix)]
310pub fn graceful_kill_process_group(
311 pid: u32,
312 initial_signal: KillSignal,
313 grace_period: std::time::Duration,
314) -> GracefulTerminationResult {
315 if !is_process_running(pid) {
317 return GracefulTerminationResult::AlreadyExited;
318 }
319
320 let target_pid = Pid::from_raw(pid as libc::pid_t);
322 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
323 return GracefulTerminationResult::AlreadyExited;
325 };
326
327 let signal = match initial_signal {
329 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
331 };
332
333 if let Err(err) = signal::killpg(pgid, signal) {
334 if err != Errno::ESRCH {
335 return GracefulTerminationResult::Error;
336 }
337 return GracefulTerminationResult::AlreadyExited;
338 }
339
340 let deadline = std::time::Instant::now() + grace_period;
342 let poll_interval = std::time::Duration::from_millis(10);
343
344 while std::time::Instant::now() < deadline {
345 if !is_process_running(pid) {
346 return GracefulTerminationResult::GracefulExit;
347 }
348 std::thread::sleep(poll_interval);
349 }
350
351 let _ = signal::killpg(pgid, Signal::SIGKILL);
355 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
356 if err == Errno::ESRCH {
357 return GracefulTerminationResult::GracefulExit;
359 }
360 return GracefulTerminationResult::Error;
361 }
362
363 GracefulTerminationResult::ForcefulKill
364}
365
366#[cfg(not(unix))]
371pub fn graceful_kill_process_group(
372 pid: u32,
373 initial_signal: KillSignal,
374 grace_period: std::time::Duration,
375) -> GracefulTerminationResult {
376 #[cfg(windows)]
377 {
378 let _ = initial_signal;
379 let pid_arg = pid.to_string();
380 match std::process::Command::new("taskkill").args(["/PID", &pid_arg, "/T"]).status() {
381 Ok(status) if status.success() => {
382 std::thread::sleep(grace_period);
383 GracefulTerminationResult::GracefulExit
384 }
385 Ok(_) => match kill_process(pid) {
386 Ok(()) => GracefulTerminationResult::ForcefulKill,
387 Err(_) => GracefulTerminationResult::AlreadyExited,
388 },
389 Err(_) => GracefulTerminationResult::Error,
390 }
391 }
392 #[cfg(not(windows))]
393 {
394 let _ = (pid, initial_signal, grace_period);
395 GracefulTerminationResult::Error
396 }
397}
398
399pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
403 graceful_kill_process_group(pid, KillSignal::Term, std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS))
404}
405
406pub async fn graceful_kill_process_group_default_async(pid: u32) -> GracefulTerminationResult {
411 tokio::task::spawn_blocking(move || graceful_kill_process_group_default(pid))
412 .await
413 .unwrap_or(GracefulTerminationResult::Error)
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn test_set_parent_death_signal_no_panic() {
422 #[cfg(target_os = "linux")]
424 {
425 let parent_pid = unistd::getpid().as_raw();
426 let _ = set_parent_death_signal(parent_pid);
429 }
430 #[cfg(not(target_os = "linux"))]
431 {
432 assert!(set_parent_death_signal(0).is_ok());
433 }
434 }
435
436 #[test]
437 fn test_kill_nonexistent_process_group() {
438 #[cfg(unix)]
441 {
442 let result = kill_process_group(2_000_000_000);
445 let _ = result;
447 }
448 #[cfg(not(unix))]
449 {
450 let result = kill_process_group(999_999);
451 assert!(result.is_ok());
452 }
453 }
454
455 #[test]
456 fn test_kill_signal_values() {
457 assert_ne!(KillSignal::Int, KillSignal::Term);
459 assert_ne!(KillSignal::Term, KillSignal::Kill);
460 assert_ne!(KillSignal::Int, KillSignal::Kill);
461
462 assert_eq!(KillSignal::default(), KillSignal::Kill);
464 }
465
466 #[test]
467 fn test_graceful_termination_result_debug() {
468 let results = [
470 GracefulTerminationResult::GracefulExit,
471 GracefulTerminationResult::ForcefulKill,
472 GracefulTerminationResult::AlreadyExited,
473 GracefulTerminationResult::Error,
474 ];
475 for result in &results {
476 let _ = format!("{result:?}");
477 }
478 }
479
480 #[test]
481 fn test_graceful_kill_nonexistent_process() {
482 let result = graceful_kill_process_group_default(2_000_000_000);
484 #[cfg(unix)]
485 {
486 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
488 }
489 #[cfg(not(unix))]
490 {
491 let _ = result;
493 }
494 }
495
496 #[tokio::test]
497 async fn test_graceful_kill_nonexistent_process_async() {
498 let result = graceful_kill_process_group_default_async(2_000_000_000).await;
499 #[cfg(unix)]
500 {
501 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
502 }
503 #[cfg(not(unix))]
504 {
505 let _ = result;
506 }
507 }
508
509 #[cfg(unix)]
510 #[test]
511 fn test_is_process_running_self() {
512 let pid = std::process::id();
514 assert!(is_process_running(pid));
515 }
516
517 #[cfg(unix)]
518 #[test]
519 fn test_is_process_running_nonexistent() {
520 assert!(!is_process_running(2_000_000_000));
522 }
523}