1use std::{
6 ffi::OsStr,
7 io::{BufRead, BufReader, Write},
8 path::{Path, PathBuf},
9 process::{Command as StdCommand, Stdio},
10 sync::Arc,
11 thread::{JoinHandle, spawn},
12};
13
14#[cfg(unix)]
15use std::os::unix::process::{CommandExt, ExitStatusExt};
16#[cfg(windows)]
17use std::os::windows::process::CommandExt;
18
19#[cfg(windows)]
20use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
21
22const NEWLINE_BYTE: u8 = b'\n';
23
24use tauri::async_runtime::{Receiver, Sender, block_on as block_on_task, channel};
25
26pub use encoding_rs::Encoding;
27use os_pipe::{PipeReader, PipeWriter, pipe};
28use serde::Serialize;
29use shared_child::SharedChild;
30use tauri::utils::platform;
31
32#[cfg(windows)]
33mod job_object;
34
35#[derive(Debug, Clone, Serialize)]
37pub struct TerminatedPayload {
38 pub code: Option<i32>,
40 pub signal: Option<i32>,
42}
43
44#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub enum CommandEvent {
48 Stderr(Vec<u8>),
51 Stdout(Vec<u8>),
54 Error(String),
56 Terminated(TerminatedPayload),
58}
59
60#[derive(Debug)]
62pub struct Command {
63 cmd: StdCommand,
64 raw_out: bool,
65 process_group: bool,
66 kill_on_drop: bool,
67}
68
69#[derive(Debug)]
71pub struct CommandChild {
72 inner: Arc<SharedChild>,
73 stdin_writer: PipeWriter,
74 #[cfg(unix)]
75 process_group: bool,
76 #[cfg(windows)]
77 job: Option<job_object::JobObject>,
78 kill_on_drop: bool,
79}
80
81impl CommandChild {
82 pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
84 self.stdin_writer.write_all(buf)?;
85 Ok(())
86 }
87
88 pub fn kill(mut self) -> crate::Result<()> {
91 self.send_kill()?;
92 self.kill_on_drop = false;
94 self.inner.wait()?;
95 Ok(())
96 }
97
98 fn send_kill(&self) -> crate::Result<()> {
100 #[cfg(unix)]
103 if self.process_group {
104 let pgid = self.inner.id() as libc::pid_t;
107 if unsafe { libc::killpg(pgid, libc::SIGKILL) } != 0 {
108 let err = std::io::Error::last_os_error();
109 if err.raw_os_error() != Some(libc::ESRCH) {
111 return Err(err.into());
112 }
113 }
114 } else {
115 self.inner.kill()?;
116 }
117
118 #[cfg(windows)]
119 match &self.job {
120 Some(job) => job.terminate()?,
121 None => self.inner.kill()?,
122 }
123
124 Ok(())
125 }
126
127 pub fn pid(&self) -> u32 {
129 self.inner.id()
130 }
131}
132
133impl Drop for CommandChild {
134 fn drop(&mut self) {
136 if self.kill_on_drop
137 && let Err(e) = self.send_kill()
138 {
139 log::error!("Failed to kill child process {}: {}", self.inner.id(), e);
140 }
141 }
142}
143
144#[derive(Debug)]
146pub struct ExitStatus {
147 code: Option<i32>,
150}
151
152impl ExitStatus {
153 pub fn code(&self) -> Option<i32> {
155 self.code
156 }
157
158 pub fn success(&self) -> bool {
160 self.code == Some(0)
161 }
162}
163
164#[derive(Debug)]
166pub struct Output {
167 pub status: ExitStatus,
169 pub stdout: Vec<u8>,
171 pub stderr: Vec<u8>,
173}
174
175fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
176 let exe_path = platform::current_exe()?;
177
178 let exe_dir = exe_path
179 .parent()
180 .ok_or(crate::Error::CurrentExeHasNoParent)?;
181
182 let base_dir = if exe_dir.ends_with("deps") {
184 exe_dir.parent().unwrap_or(exe_dir)
185 } else {
186 exe_dir
187 };
188
189 let mut command_path = base_dir.join(command);
190
191 #[cfg(windows)]
192 {
193 let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
194 if !already_exe {
195 command_path.as_mut_os_string().push(".exe");
197 }
198 }
199
200 #[cfg(not(windows))]
201 {
202 if command_path.extension().is_some_and(|ext| ext == "exe") {
203 command_path.set_extension("");
204 }
205 }
206
207 Ok(command_path)
208}
209
210impl From<Command> for StdCommand {
211 fn from(cmd: Command) -> StdCommand {
212 cmd.cmd
213 }
214}
215
216impl Command {
217 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
218 log::debug!(
219 "Creating sidecar {}",
220 program.as_ref().to_str().unwrap_or("")
221 );
222 let mut command = StdCommand::new(program);
223
224 command.stdout(Stdio::piped());
225 command.stdin(Stdio::piped());
226 command.stderr(Stdio::piped());
227 #[cfg(windows)]
228 command.creation_flags(CREATE_NO_WINDOW);
229
230 Self {
231 cmd: command,
232 raw_out: false,
233 process_group: false,
234 kill_on_drop: false,
235 }
236 }
237
238 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
239 Ok(Self::new(relative_command_path(program.as_ref())?))
240 }
241
242 #[must_use]
244 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
245 self.cmd.arg(arg);
246 self
247 }
248
249 #[must_use]
251 pub fn args<I, S>(mut self, args: I) -> Self
252 where
253 I: IntoIterator<Item = S>,
254 S: AsRef<OsStr>,
255 {
256 self.cmd.args(args);
257 self
258 }
259
260 #[must_use]
262 pub fn env_clear(mut self) -> Self {
263 self.cmd.env_clear();
264 self
265 }
266
267 #[must_use]
269 pub fn env<K, V>(mut self, key: K, value: V) -> Self
270 where
271 K: AsRef<OsStr>,
272 V: AsRef<OsStr>,
273 {
274 self.cmd.env(key, value);
275 self
276 }
277
278 #[must_use]
280 pub fn envs<I, K, V>(mut self, envs: I) -> Self
281 where
282 I: IntoIterator<Item = (K, V)>,
283 K: AsRef<OsStr>,
284 V: AsRef<OsStr>,
285 {
286 self.cmd.envs(envs);
287 self
288 }
289
290 #[must_use]
292 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
293 self.cmd.current_dir(current_dir);
294 self
295 }
296
297 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
299 self.raw_out = raw_out;
300 self
301 }
302
303 #[must_use]
308 pub fn set_process_group(mut self, process_group: bool) -> Self {
309 self.process_group = process_group;
310 self
311 }
312
313 #[must_use]
323 pub fn kill_on_drop(mut self, kill_on_drop: bool) -> Self {
324 self.kill_on_drop = kill_on_drop;
325 self
326 }
327
328 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
388 let raw = self.raw_out;
389 let process_group = self.process_group;
390 let kill_on_drop = self.kill_on_drop;
391 let mut command: StdCommand = self.into();
392 let (stdout_reader, stdout_writer) = pipe()?;
393 let (stderr_reader, stderr_writer) = pipe()?;
394 let (stdin_reader, stdin_writer) = pipe()?;
395 command.stdout(stdout_writer);
396 command.stderr(stderr_writer);
397 command.stdin(stdin_reader);
398
399 #[cfg(unix)]
400 if process_group {
401 command.process_group(0);
402 }
403 #[cfg(windows)]
404 if process_group {
405 command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
407 }
408
409 let child = command.spawn()?;
410
411 #[cfg(windows)]
412 let job = if process_group {
413 match job_object::JobObject::assign(&child) {
414 Ok(job) => Some(job),
415 Err(e) => {
416 let mut child = child;
418 let _ = child.kill();
419 let _ = child.wait();
420 return Err(e.into());
421 }
422 }
423 } else {
424 None
425 };
426
427 let child = Arc::new(SharedChild::new(child)?);
428 let child_ = child.clone();
429 let (tx, rx) = channel(1);
430
431 let stdout_thread = spawn_pipe_reader(tx.clone(), stdout_reader, CommandEvent::Stdout, raw);
432 let stderr_thread = spawn_pipe_reader(tx.clone(), stderr_reader, CommandEvent::Stderr, raw);
433
434 spawn(move || {
435 let status = child_.wait();
436 let _ = stdout_thread.join();
439 let _ = stderr_thread.join();
440 let _ = match status {
441 Ok(status) => block_on_task(async move {
442 tx.send(CommandEvent::Terminated(TerminatedPayload {
443 code: status.code(),
444 #[cfg(windows)]
445 signal: None,
446 #[cfg(unix)]
447 signal: status.signal(),
448 }))
449 .await
450 }),
451 Err(e) => {
452 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
453 }
454 };
455 });
456
457 Ok((
458 rx,
459 CommandChild {
460 inner: child,
461 stdin_writer,
462 #[cfg(unix)]
463 process_group,
464 #[cfg(windows)]
465 job,
466 kill_on_drop,
467 },
468 ))
469 }
470
471 pub async fn status(self) -> crate::Result<ExitStatus> {
485 let (mut rx, _child) = self.spawn()?;
486 let mut code = None;
487 #[allow(clippy::collapsible_match)]
488 while let Some(event) = rx.recv().await {
489 if let CommandEvent::Terminated(payload) = event {
490 code = payload.code;
491 }
492 }
493 Ok(ExitStatus { code })
494 }
495
496 pub async fn output(self) -> crate::Result<Output> {
512 let (mut rx, _child) = self.spawn()?;
513
514 let mut code = None;
515 let mut stdout = Vec::new();
516 let mut stderr = Vec::new();
517
518 while let Some(event) = rx.recv().await {
519 match event {
520 CommandEvent::Terminated(payload) => {
521 code = payload.code;
522 }
523 CommandEvent::Stdout(line) => {
524 stdout.extend(line);
525 stdout.push(NEWLINE_BYTE);
526 }
527 CommandEvent::Stderr(line) => {
528 stderr.extend(line);
529 stderr.push(NEWLINE_BYTE);
530 }
531 CommandEvent::Error(_) => {}
532 }
533 }
534 Ok(Output {
535 status: ExitStatus { code },
536 stdout,
537 stderr,
538 })
539 }
540}
541
542fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
543 mut reader: BufReader<PipeReader>,
544 tx: Sender<CommandEvent>,
545 wrapper: F,
546) {
547 loop {
548 let result = reader.fill_buf();
549 match result {
550 Ok(buf) => {
551 let length = buf.len();
552 if length == 0 {
553 break;
554 }
555 let tx_ = tx.clone();
556 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
557 reader.consume(length);
558 }
559 Err(e) => {
560 let tx_ = tx.clone();
561 let _ = block_on_task(
562 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
563 );
564 }
565 }
566 }
567}
568
569fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
570 mut reader: BufReader<PipeReader>,
571 tx: Sender<CommandEvent>,
572 wrapper: F,
573) {
574 loop {
575 let mut buf = Vec::new();
576 match tauri::utils::io::read_line(&mut reader, &mut buf) {
577 Ok(n) => {
578 if n == 0 {
579 break;
580 }
581 let tx_ = tx.clone();
582 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
583 }
584 Err(e) => {
585 let _ =
586 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await });
587 break;
588 }
589 }
590 }
591}
592
593fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
594 tx: Sender<CommandEvent>,
595 pipe_reader: PipeReader,
596 wrapper: F,
597 raw_out: bool,
598) -> JoinHandle<()> {
599 spawn(move || {
600 let reader = BufReader::new(pipe_reader);
601
602 if raw_out {
603 read_raw_bytes(reader, tx, wrapper);
604 } else {
605 read_line(reader, tx, wrapper);
606 }
607 })
608}
609
610#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn relative_command_path_resolves() {
617 let cwd_parent = platform::current_exe()
618 .unwrap()
619 .parent()
620 .unwrap()
621 .parent() .unwrap()
623 .to_owned();
624 assert_eq!(
625 relative_command_path(Path::new("Tauri.Example")).unwrap(),
626 cwd_parent.join(if cfg!(windows) {
627 "Tauri.Example.exe"
628 } else {
629 "Tauri.Example"
630 })
631 );
632 assert_eq!(
633 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
634 cwd_parent.join(if cfg!(windows) {
635 "Tauri.Example.exe"
636 } else {
637 "Tauri.Example"
638 })
639 );
640 }
641
642 #[cfg(not(windows))]
643 #[test]
644 fn test_cmd_spawn_output() {
645 let cmd = Command::new("cat").args(["test/test.txt"]);
646 let (mut rx, _) = cmd.spawn().unwrap();
647
648 tauri::async_runtime::block_on(async move {
649 while let Some(event) = rx.recv().await {
650 match event {
651 CommandEvent::Terminated(payload) => {
652 assert_eq!(payload.code, Some(0));
653 }
654 CommandEvent::Stdout(line) => {
655 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
656 }
657 _ => {}
658 }
659 }
660 });
661 }
662
663 #[cfg(not(windows))]
664 #[test]
665 fn test_cmd_spawn_raw_output() {
666 let cmd = Command::new("cat").args(["test/test.txt"]);
667 let (mut rx, _) = cmd.spawn().unwrap();
668
669 tauri::async_runtime::block_on(async move {
670 while let Some(event) = rx.recv().await {
671 match event {
672 CommandEvent::Terminated(payload) => {
673 assert_eq!(payload.code, Some(0));
674 }
675 CommandEvent::Stdout(line) => {
676 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
677 }
678 _ => {}
679 }
680 }
681 });
682 }
683
684 #[cfg(not(windows))]
685 #[test]
686 fn test_cmd_spawn_fail() {
688 let cmd = Command::new("cat").args(["test/"]);
689 let (mut rx, _) = cmd.spawn().unwrap();
690
691 tauri::async_runtime::block_on(async move {
692 while let Some(event) = rx.recv().await {
693 match event {
694 CommandEvent::Terminated(payload) => {
695 assert_eq!(payload.code, Some(1));
696 }
697 CommandEvent::Stderr(line) => {
698 assert_eq!(
699 String::from_utf8(line).unwrap(),
700 "cat: test/: Is a directory\n"
701 );
702 }
703 _ => {}
704 }
705 }
706 });
707 }
708
709 #[cfg(not(windows))]
710 #[test]
711 fn test_cmd_spawn_raw_fail() {
713 let cmd = Command::new("cat").args(["test/"]);
714 let (mut rx, _) = cmd.spawn().unwrap();
715
716 tauri::async_runtime::block_on(async move {
717 while let Some(event) = rx.recv().await {
718 match event {
719 CommandEvent::Terminated(payload) => {
720 assert_eq!(payload.code, Some(1));
721 }
722 CommandEvent::Stderr(line) => {
723 assert_eq!(
724 String::from_utf8(line).unwrap(),
725 "cat: test/: Is a directory\n"
726 );
727 }
728 _ => {}
729 }
730 }
731 });
732 }
733
734 #[cfg(not(windows))]
735 #[test]
736 fn test_cmd_output_output() {
737 let cmd = Command::new("cat").args(["test/test.txt"]);
738 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
739
740 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
741 assert_eq!(
742 String::from_utf8(output.stdout).unwrap(),
743 "This is a test doc!\n"
744 );
745 }
746
747 #[cfg(not(windows))]
748 #[test]
749 fn test_cmd_output_output_fail() {
750 let cmd = Command::new("cat").args(["test/"]);
751 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
752
753 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
754 assert_eq!(
755 String::from_utf8(output.stderr).unwrap(),
756 "cat: test/: Is a directory\n\n"
757 );
758 }
759
760 #[cfg(not(windows))]
761 #[test]
762 fn test_cmd_spawn_process_group_output() {
763 let cmd = Command::new("cat")
764 .args(["test/test.txt"])
765 .set_process_group(true);
766 let (mut rx, _) = cmd.spawn().unwrap();
767
768 tauri::async_runtime::block_on(async move {
769 while let Some(event) = rx.recv().await {
770 match event {
771 CommandEvent::Terminated(payload) => {
772 assert_eq!(payload.code, Some(0));
773 }
774 CommandEvent::Stdout(line) => {
775 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
776 }
777 _ => {}
778 }
779 }
780 });
781 }
782
783 #[cfg(not(windows))]
784 #[test]
785 fn test_cmd_process_group_kill() {
786 let cmd = Command::new("sh")
789 .args(["-c", "sleep 60"])
790 .set_process_group(true);
791 let (mut rx, child) = cmd.spawn().unwrap();
792 let pid = child.pid();
793
794 let ret = unsafe { libc::kill(pid as i32, 0) };
796 assert_eq!(ret, 0, "process should be running");
797
798 child.kill().unwrap();
800
801 tauri::async_runtime::block_on(async move {
802 while let Some(event) = rx.recv().await {
803 if let CommandEvent::Terminated(payload) = event {
804 assert!(payload.code.is_none() || payload.signal.is_some());
806 break;
807 }
808 }
809 });
810
811 let ret = unsafe { libc::killpg(pid as i32, 0) };
813 assert_ne!(ret, 0, "process group should no longer exist");
814 }
815
816 #[cfg(not(windows))]
817 fn wait_terminated(mut rx: Receiver<CommandEvent>) -> TerminatedPayload {
818 tauri::async_runtime::block_on(async move {
819 while let Some(event) = rx.recv().await {
820 if let CommandEvent::Terminated(payload) = event {
821 return payload;
822 }
823 }
824 panic!("channel closed without a Terminated event");
825 })
826 }
827
828 #[cfg(not(windows))]
829 #[test]
830 fn test_cmd_kill_on_drop() {
831 let (rx, child) = Command::new("sleep")
832 .args(["60"])
833 .kill_on_drop(true)
834 .spawn()
835 .unwrap();
836 drop(child);
837
838 let payload = wait_terminated(rx);
839 assert_eq!(payload.signal, Some(libc::SIGKILL));
840 }
841
842 #[cfg(not(windows))]
843 #[test]
844 fn test_cmd_no_kill_on_drop_by_default() {
845 let (_rx, child) = Command::new("sleep").args(["60"]).spawn().unwrap();
846 let pid = child.pid();
847 drop(child);
848
849 std::thread::sleep(std::time::Duration::from_millis(200));
850 let ret = unsafe { libc::kill(pid as i32, 0) };
851 assert_eq!(ret, 0, "process should still be running");
852 unsafe { libc::kill(pid as i32, libc::SIGKILL) };
853 }
854
855 #[cfg(not(windows))]
856 #[test]
857 fn test_cmd_kill_on_drop_process_group() {
858 let (rx, child) = Command::new("sh")
860 .args(["-c", "sleep 60; true"])
861 .set_process_group(true)
862 .kill_on_drop(true)
863 .spawn()
864 .unwrap();
865 let pid = child.pid();
866 drop(child);
867
868 wait_terminated(rx);
869 let ret = unsafe { libc::killpg(pid as i32, 0) };
870 assert_ne!(ret, 0, "process group should no longer exist");
871 }
872
873 #[test]
874 fn test_cmd_process_group_output() {
875 #[cfg(not(windows))]
877 let cmd = Command::new("cat").args(["test/test.txt"]);
878 #[cfg(windows)]
879 let cmd = Command::new("cmd").args(["/C", "type test\\test.txt"]);
880
881 let output = tauri::async_runtime::block_on(cmd.set_process_group(true).output()).unwrap();
882
883 assert!(output.status.success());
884 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
885 assert_eq!(
886 String::from_utf8(output.stdout).unwrap().trim(),
887 "This is a test doc!"
888 );
889 }
890
891 #[test]
892 fn test_cmd_kill_after_exit() {
893 for process_group in [false, true] {
894 #[cfg(not(windows))]
895 let cmd = Command::new("true");
896 #[cfg(windows)]
897 let cmd = Command::new("cmd").args(["/C", "exit 0"]);
898 let (rx, child) = cmd.set_process_group(process_group).spawn().unwrap();
899 wait_for_terminated(rx);
900 child.kill().unwrap();
901 }
902 }
903
904 fn sim_command(script: &str) -> Command {
906 if cfg!(windows) {
907 Command::new("powershell").args([
908 "-NoProfile".to_string(),
909 "-ExecutionPolicy".to_string(),
910 "Bypass".to_string(),
911 "-File".to_string(),
912 format!("test/{script}.ps1"),
913 ])
914 } else {
915 Command::new("sh").args([format!("test/{script}.sh")])
916 }
917 }
918
919 fn read_grandchild_pid(rx: &mut Receiver<CommandEvent>) -> u32 {
921 tauri::async_runtime::block_on(async {
922 let mut pid = None;
923 while let Some(event) = rx.recv().await {
924 if let CommandEvent::Stdout(line) = &event {
925 let line_str = String::from_utf8_lossy(line);
926 if let Some(rest) = line_str.strip_prefix("CHILD_PID=") {
927 pid = rest.trim().parse::<u32>().ok();
928 }
929 }
930 if pid.is_some() {
931 break;
932 }
933 }
934 pid.expect("should have received CHILD_PID from script")
935 })
936 }
937
938 fn wait_for_terminated(mut rx: Receiver<CommandEvent>) {
940 let got = tauri::async_runtime::block_on(async move {
941 while let Some(event) = rx.recv().await {
942 if let CommandEvent::Terminated(_) = event {
943 return true;
944 }
945 }
946 false
947 });
948 assert!(got, "expected a Terminated event");
949 }
950
951 #[cfg(not(windows))]
952 fn pid_alive(pid: u32) -> bool {
953 unsafe { libc::kill(pid as i32, 0) == 0 }
954 }
955
956 #[cfg(windows)]
957 fn pid_alive(pid: u32) -> bool {
958 use windows_sys::Win32::{
959 Foundation::{CloseHandle, STILL_ACTIVE},
960 System::Threading::{
961 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
962 },
963 };
964 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
965 if handle.is_null() {
966 return false;
967 }
968 let mut code = 0u32;
969 let alive =
970 unsafe { GetExitCodeProcess(handle, &mut code) } != 0 && code == STILL_ACTIVE as u32;
971 unsafe { CloseHandle(handle) };
972 alive
973 }
974
975 #[cfg(not(windows))]
976 fn force_kill(pid: u32) {
977 unsafe { libc::kill(pid as i32, libc::SIGKILL) };
978 }
979
980 #[cfg(windows)]
981 fn force_kill(pid: u32) {
982 use windows_sys::Win32::{
983 Foundation::CloseHandle,
984 System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess},
985 };
986 let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
987 if !handle.is_null() {
988 unsafe { TerminateProcess(handle, 1) };
989 unsafe { CloseHandle(handle) };
990 }
991 }
992
993 #[test]
1000 fn test_pyinstaller_simulation_without_process_group() {
1001 let (mut rx, child) = sim_command("pyinstaller_sim").spawn().unwrap();
1003
1004 let grandchild_pid = read_grandchild_pid(&mut rx);
1005 assert!(
1006 pid_alive(grandchild_pid),
1007 "grandchild should be running before kill"
1008 );
1009
1010 child.kill().unwrap();
1012 std::thread::sleep(std::time::Duration::from_millis(100));
1013
1014 assert!(
1016 pid_alive(grandchild_pid),
1017 "grandchild should survive when process_group is off"
1018 );
1019
1020 force_kill(grandchild_pid);
1023 wait_for_terminated(rx);
1024 }
1025
1026 #[test]
1027 fn test_pyinstaller_simulation_with_process_group() {
1028 let (mut rx, child) = sim_command("pyinstaller_sim")
1030 .set_process_group(true)
1031 .spawn()
1032 .unwrap();
1033
1034 let grandchild_pid = read_grandchild_pid(&mut rx);
1035 assert!(
1036 pid_alive(grandchild_pid),
1037 "grandchild should be running before kill"
1038 );
1039
1040 child.kill().unwrap();
1042 std::thread::sleep(std::time::Duration::from_millis(100));
1043
1044 assert!(
1046 !pid_alive(grandchild_pid),
1047 "grandchild should be killed when process_group is on"
1048 );
1049
1050 wait_for_terminated(rx);
1051 }
1052
1053 #[test]
1056 fn test_process_group_kill_after_leader_exit() {
1057 let (mut rx, child) = sim_command("launcher_sim")
1058 .set_process_group(true)
1059 .spawn()
1060 .unwrap();
1061
1062 let grandchild_pid = read_grandchild_pid(&mut rx);
1063
1064 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
1066 while child.inner.try_wait().unwrap().is_none() {
1067 assert!(
1068 std::time::Instant::now() < deadline,
1069 "launcher should exit on its own"
1070 );
1071 std::thread::sleep(std::time::Duration::from_millis(50));
1072 }
1073 assert!(
1074 pid_alive(grandchild_pid),
1075 "grandchild should outlive the launcher"
1076 );
1077
1078 child.kill().unwrap();
1079 std::thread::sleep(std::time::Duration::from_millis(100));
1080
1081 let alive = pid_alive(grandchild_pid);
1082 if alive {
1083 force_kill(grandchild_pid);
1084 }
1085 assert!(
1086 !alive,
1087 "grandchild should be killed even though the launcher already exited"
1088 );
1089 }
1090}