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)]
62fn nix_err_to_io(err: Errno) -> io::Error {
63 io::Error::from_raw_os_error(err as i32)
64}
65
66#[cfg(target_os = "linux")]
71pub fn set_parent_death_signal(parent_pid: libc::pid_t) -> io::Result<()> {
72 prctl::set_pdeathsig(Some(Signal::SIGTERM)).map_err(nix_err_to_io)?;
73
74 if unistd::getppid() != Pid::from_raw(parent_pid) {
76 signal::kill(unistd::getpid(), Signal::SIGTERM).map_err(nix_err_to_io)?;
77 }
78
79 Ok(())
80}
81
82#[cfg(not(target_os = "linux"))]
84pub fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()> {
85 Ok(())
86}
87
88#[cfg(unix)]
93pub fn detach_from_tty() -> io::Result<()> {
94 match unistd::setsid() {
95 Ok(_) => Ok(()),
96 Err(Errno::EPERM) => set_process_group(),
98 Err(err) => Err(nix_err_to_io(err)),
99 }
100}
101
102#[cfg(not(unix))]
104pub fn detach_from_tty() -> io::Result<()> {
105 Ok(())
106}
107
108#[cfg(unix)]
112pub fn set_process_group() -> io::Result<()> {
113 unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(nix_err_to_io)
114}
115
116#[cfg(not(unix))]
118pub fn set_process_group() -> io::Result<()> {
119 Ok(())
120}
121
122#[cfg(unix)]
126pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> {
127 kill_process_group_by_pid_with_signal(pid, KillSignal::Kill)
128}
129
130#[cfg(unix)]
132pub fn kill_process_group_by_pid_with_signal(pid: u32, signal: KillSignal) -> io::Result<()> {
133 use std::io::ErrorKind;
134
135 let target_pid = Pid::from_raw(pid as libc::pid_t);
136 let pgid = unistd::getpgid(Some(target_pid));
137 let mut pgid_err = None;
138
139 match pgid {
140 Ok(group) => {
141 if let Err(err) = signal::killpg(group, signal.as_nix_signal()) {
142 let io_err = nix_err_to_io(err);
143 if io_err.kind() != ErrorKind::NotFound {
144 pgid_err = Some(io_err);
145 }
146 }
147 }
148 Err(err) => pgid_err = Some(nix_err_to_io(err)),
149 }
150
151 if let Err(err) = signal::kill(target_pid, signal.as_nix_signal()) {
155 let io_err = nix_err_to_io(err);
156 if io_err.kind() == ErrorKind::NotFound {
157 return Ok(());
159 }
160 if let Some(pgid_error) = pgid_err {
162 return Err(pgid_error);
163 }
164 return Err(io_err);
165 }
166
167 Ok(())
168}
169
170#[cfg(not(unix))]
172pub fn kill_process_group_by_pid(_pid: u32) -> io::Result<()> {
173 Ok(())
174}
175
176#[cfg(not(unix))]
178pub fn kill_process_group_by_pid_with_signal(_pid: u32, _signal: KillSignal) -> io::Result<()> {
179 Ok(())
180}
181
182#[cfg(unix)]
184pub fn kill_process_group(process_group_id: u32) -> io::Result<()> {
185 kill_process_group_with_signal(process_group_id, KillSignal::Kill)
186}
187
188#[cfg(unix)]
190pub fn kill_process_group_with_signal(process_group_id: u32, signal: KillSignal) -> io::Result<()> {
191 use std::io::ErrorKind;
192
193 let pgid = Pid::from_raw(process_group_id as libc::pid_t);
194 if let Err(err) = signal::killpg(pgid, signal.as_nix_signal()) {
195 let io_err = nix_err_to_io(err);
196 if io_err.kind() != ErrorKind::NotFound {
197 return Err(io_err);
198 }
199 }
200
201 Ok(())
202}
203
204#[cfg(not(unix))]
206pub fn kill_process_group(_process_group_id: u32) -> io::Result<()> {
207 Ok(())
208}
209
210#[cfg(not(unix))]
212pub fn kill_process_group_with_signal(
213 _process_group_id: u32,
214 _signal: KillSignal,
215) -> io::Result<()> {
216 Ok(())
217}
218
219#[cfg(unix)]
221pub fn kill_child_process_group(child: &mut Child) -> io::Result<()> {
222 kill_child_process_group_with_signal(child, KillSignal::Kill)
223}
224
225#[cfg(unix)]
227pub fn kill_child_process_group_with_signal(
228 child: &mut Child,
229 signal: KillSignal,
230) -> io::Result<()> {
231 if let Some(pid) = child.id() {
232 return kill_process_group_by_pid_with_signal(pid, signal);
233 }
234
235 Ok(())
236}
237
238#[cfg(not(unix))]
240pub fn kill_child_process_group(_child: &mut tokio::process::Child) -> io::Result<()> {
241 Ok(())
242}
243
244#[cfg(not(unix))]
246pub fn kill_child_process_group_with_signal(
247 _child: &mut tokio::process::Child,
248 _signal: KillSignal,
249) -> io::Result<()> {
250 Ok(())
251}
252
253#[cfg(windows)]
255pub fn kill_process(pid: u32) -> io::Result<()> {
256 let status = std::process::Command::new("taskkill")
257 .args(["/PID", &pid.to_string(), "/T", "/F"])
258 .status()?;
259 if status.success() {
260 Ok(())
261 } else {
262 Err(io::Error::other("taskkill failed"))
263 }
264}
265
266#[cfg(not(windows))]
268pub fn kill_process(_pid: u32) -> io::Result<()> {
269 Ok(())
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum GracefulTerminationResult {
275 GracefulExit,
277 ForcefulKill,
279 AlreadyExited,
281 Error,
283}
284
285#[cfg(unix)]
287fn is_process_running(pid: u32) -> bool {
288 let target_pid = Pid::from_raw(pid as libc::pid_t);
289 match signal::kill(target_pid, None::<Signal>) {
290 Ok(()) => true,
291 Err(Errno::EPERM) => true,
293 Err(_) => false,
294 }
295}
296
297#[cfg(not(unix))]
298fn is_process_running(_pid: u32) -> bool {
299 true
301}
302
303#[cfg(unix)]
317pub fn graceful_kill_process_group(
318 pid: u32,
319 initial_signal: KillSignal,
320 grace_period: std::time::Duration,
321) -> GracefulTerminationResult {
322 if !is_process_running(pid) {
324 return GracefulTerminationResult::AlreadyExited;
325 }
326
327 let target_pid = Pid::from_raw(pid as libc::pid_t);
329 let Ok(pgid) = unistd::getpgid(Some(target_pid)) else {
330 return GracefulTerminationResult::AlreadyExited;
332 };
333
334 let signal = match initial_signal {
336 KillSignal::Kill => Signal::SIGTERM, other => other.as_nix_signal(),
338 };
339
340 if let Err(err) = signal::killpg(pgid, signal) {
341 if err != Errno::ESRCH {
342 return GracefulTerminationResult::Error;
343 }
344 return GracefulTerminationResult::AlreadyExited;
345 }
346
347 let deadline = std::time::Instant::now() + grace_period;
349 let poll_interval = std::time::Duration::from_millis(10);
350
351 while std::time::Instant::now() < deadline {
352 if !is_process_running(pid) {
353 return GracefulTerminationResult::GracefulExit;
354 }
355 std::thread::sleep(poll_interval);
356 }
357
358 let _ = signal::killpg(pgid, Signal::SIGKILL);
362 if let Err(err) = signal::kill(target_pid, Signal::SIGKILL) {
363 if err == Errno::ESRCH {
364 return GracefulTerminationResult::GracefulExit;
366 }
367 return GracefulTerminationResult::Error;
368 }
369
370 GracefulTerminationResult::ForcefulKill
371}
372
373#[cfg(not(unix))]
378pub fn graceful_kill_process_group(
379 pid: u32,
380 initial_signal: KillSignal,
381 grace_period: std::time::Duration,
382) -> GracefulTerminationResult {
383 #[cfg(windows)]
384 {
385 let _ = initial_signal;
386 let pid_arg = pid.to_string();
387 match std::process::Command::new("taskkill")
388 .args(["/PID", &pid_arg, "/T"])
389 .status()
390 {
391 Ok(status) if status.success() => {
392 std::thread::sleep(grace_period);
393 GracefulTerminationResult::GracefulExit
394 }
395 Ok(_) => match kill_process(pid) {
396 Ok(()) => GracefulTerminationResult::ForcefulKill,
397 Err(_) => GracefulTerminationResult::AlreadyExited,
398 },
399 Err(_) => GracefulTerminationResult::Error,
400 }
401 }
402 #[cfg(not(windows))]
403 {
404 let _ = (pid, initial_signal, grace_period);
405 GracefulTerminationResult::Error
406 }
407}
408
409#[cfg(unix)]
413pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
414 graceful_kill_process_group(
415 pid,
416 KillSignal::Term,
417 std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS),
418 )
419}
420
421#[cfg(not(unix))]
423pub fn graceful_kill_process_group_default(pid: u32) -> GracefulTerminationResult {
424 graceful_kill_process_group(
425 pid,
426 KillSignal::Term,
427 std::time::Duration::from_millis(DEFAULT_GRACEFUL_TIMEOUT_MS),
428 )
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn test_set_parent_death_signal_no_panic() {
437 #[cfg(target_os = "linux")]
439 {
440 let parent_pid = unistd::getpid().as_raw();
441 let _ = set_parent_death_signal(parent_pid);
444 }
445 #[cfg(not(target_os = "linux"))]
446 {
447 assert!(set_parent_death_signal(0).is_ok());
448 }
449 }
450
451 #[test]
452 fn test_kill_nonexistent_process_group() {
453 #[cfg(unix)]
456 {
457 let result = kill_process_group(2_000_000_000);
460 let _ = result;
462 }
463 #[cfg(not(unix))]
464 {
465 let result = kill_process_group(999_999);
466 assert!(result.is_ok());
467 }
468 }
469
470 #[test]
471 fn test_kill_signal_values() {
472 assert_ne!(KillSignal::Int, KillSignal::Term);
474 assert_ne!(KillSignal::Term, KillSignal::Kill);
475 assert_ne!(KillSignal::Int, KillSignal::Kill);
476
477 assert_eq!(KillSignal::default(), KillSignal::Kill);
479 }
480
481 #[test]
482 fn test_graceful_termination_result_debug() {
483 let results = [
485 GracefulTerminationResult::GracefulExit,
486 GracefulTerminationResult::ForcefulKill,
487 GracefulTerminationResult::AlreadyExited,
488 GracefulTerminationResult::Error,
489 ];
490 for result in &results {
491 let _ = format!("{result:?}");
492 }
493 }
494
495 #[test]
496 fn test_graceful_kill_nonexistent_process() {
497 let result = graceful_kill_process_group_default(2_000_000_000);
499 #[cfg(unix)]
500 {
501 assert_eq!(result, GracefulTerminationResult::AlreadyExited);
503 }
504 #[cfg(not(unix))]
505 {
506 let _ = result;
508 }
509 }
510
511 #[cfg(unix)]
512 #[test]
513 fn test_is_process_running_self() {
514 let pid = std::process::id();
516 assert!(is_process_running(pid));
517 }
518
519 #[cfg(unix)]
520 #[test]
521 fn test_is_process_running_nonexistent() {
522 assert!(!is_process_running(2_000_000_000));
524 }
525}