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}
67
68#[derive(Debug)]
70pub struct CommandChild {
71 inner: Arc<SharedChild>,
72 stdin_writer: PipeWriter,
73 #[cfg(unix)]
74 process_group: bool,
75 #[cfg(windows)]
76 job: Option<job_object::JobObject>,
77}
78
79impl CommandChild {
80 pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
82 self.stdin_writer.write_all(buf)?;
83 Ok(())
84 }
85
86 pub fn kill(self) -> crate::Result<()> {
89 #[cfg(unix)]
92 if self.process_group {
93 let pgid = self.inner.id() as libc::pid_t;
96 if unsafe { libc::killpg(pgid, libc::SIGKILL) } != 0 {
97 let err = std::io::Error::last_os_error();
98 if err.raw_os_error() != Some(libc::ESRCH) {
100 return Err(err.into());
101 }
102 }
103 } else {
104 self.inner.kill()?;
105 }
106
107 #[cfg(windows)]
108 match &self.job {
109 Some(job) => job.terminate()?,
110 None => self.inner.kill()?,
111 }
112
113 self.inner.wait()?;
114 Ok(())
115 }
116
117 pub fn pid(&self) -> u32 {
119 self.inner.id()
120 }
121}
122
123#[derive(Debug)]
125pub struct ExitStatus {
126 code: Option<i32>,
129}
130
131impl ExitStatus {
132 pub fn code(&self) -> Option<i32> {
134 self.code
135 }
136
137 pub fn success(&self) -> bool {
139 self.code == Some(0)
140 }
141}
142
143#[derive(Debug)]
145pub struct Output {
146 pub status: ExitStatus,
148 pub stdout: Vec<u8>,
150 pub stderr: Vec<u8>,
152}
153
154fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
155 let exe_path = platform::current_exe()?;
156
157 let exe_dir = exe_path
158 .parent()
159 .ok_or(crate::Error::CurrentExeHasNoParent)?;
160
161 let base_dir = if exe_dir.ends_with("deps") {
163 exe_dir.parent().unwrap_or(exe_dir)
164 } else {
165 exe_dir
166 };
167
168 let mut command_path = base_dir.join(command);
169
170 #[cfg(windows)]
171 {
172 let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
173 if !already_exe {
174 command_path.as_mut_os_string().push(".exe");
176 }
177 }
178
179 #[cfg(not(windows))]
180 {
181 if command_path.extension().is_some_and(|ext| ext == "exe") {
182 command_path.set_extension("");
183 }
184 }
185
186 Ok(command_path)
187}
188
189impl From<Command> for StdCommand {
190 fn from(cmd: Command) -> StdCommand {
191 cmd.cmd
192 }
193}
194
195impl Command {
196 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
197 log::debug!(
198 "Creating sidecar {}",
199 program.as_ref().to_str().unwrap_or("")
200 );
201 let mut command = StdCommand::new(program);
202
203 command.stdout(Stdio::piped());
204 command.stdin(Stdio::piped());
205 command.stderr(Stdio::piped());
206 #[cfg(windows)]
207 command.creation_flags(CREATE_NO_WINDOW);
208
209 Self {
210 cmd: command,
211 raw_out: false,
212 process_group: false,
213 }
214 }
215
216 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
217 Ok(Self::new(relative_command_path(program.as_ref())?))
218 }
219
220 #[must_use]
222 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
223 self.cmd.arg(arg);
224 self
225 }
226
227 #[must_use]
229 pub fn args<I, S>(mut self, args: I) -> Self
230 where
231 I: IntoIterator<Item = S>,
232 S: AsRef<OsStr>,
233 {
234 self.cmd.args(args);
235 self
236 }
237
238 #[must_use]
240 pub fn env_clear(mut self) -> Self {
241 self.cmd.env_clear();
242 self
243 }
244
245 #[must_use]
247 pub fn env<K, V>(mut self, key: K, value: V) -> Self
248 where
249 K: AsRef<OsStr>,
250 V: AsRef<OsStr>,
251 {
252 self.cmd.env(key, value);
253 self
254 }
255
256 #[must_use]
258 pub fn envs<I, K, V>(mut self, envs: I) -> Self
259 where
260 I: IntoIterator<Item = (K, V)>,
261 K: AsRef<OsStr>,
262 V: AsRef<OsStr>,
263 {
264 self.cmd.envs(envs);
265 self
266 }
267
268 #[must_use]
270 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
271 self.cmd.current_dir(current_dir);
272 self
273 }
274
275 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
277 self.raw_out = raw_out;
278 self
279 }
280
281 #[must_use]
286 pub fn set_process_group(mut self, process_group: bool) -> Self {
287 self.process_group = process_group;
288 self
289 }
290
291 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
351 let raw = self.raw_out;
352 let process_group = self.process_group;
353 let mut command: StdCommand = self.into();
354 let (stdout_reader, stdout_writer) = pipe()?;
355 let (stderr_reader, stderr_writer) = pipe()?;
356 let (stdin_reader, stdin_writer) = pipe()?;
357 command.stdout(stdout_writer);
358 command.stderr(stderr_writer);
359 command.stdin(stdin_reader);
360
361 #[cfg(unix)]
362 if process_group {
363 command.process_group(0);
364 }
365 #[cfg(windows)]
366 if process_group {
367 command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
369 }
370
371 let child = command.spawn()?;
372
373 #[cfg(windows)]
374 let job = if process_group {
375 match job_object::JobObject::assign(&child) {
376 Ok(job) => Some(job),
377 Err(e) => {
378 let mut child = child;
380 let _ = child.kill();
381 let _ = child.wait();
382 return Err(e.into());
383 }
384 }
385 } else {
386 None
387 };
388
389 let child = Arc::new(SharedChild::new(child)?);
390 let child_ = child.clone();
391 let (tx, rx) = channel(1);
392
393 let stdout_thread = spawn_pipe_reader(tx.clone(), stdout_reader, CommandEvent::Stdout, raw);
394 let stderr_thread = spawn_pipe_reader(tx.clone(), stderr_reader, CommandEvent::Stderr, raw);
395
396 spawn(move || {
397 let status = child_.wait();
398 let _ = stdout_thread.join();
401 let _ = stderr_thread.join();
402 let _ = match status {
403 Ok(status) => block_on_task(async move {
404 tx.send(CommandEvent::Terminated(TerminatedPayload {
405 code: status.code(),
406 #[cfg(windows)]
407 signal: None,
408 #[cfg(unix)]
409 signal: status.signal(),
410 }))
411 .await
412 }),
413 Err(e) => {
414 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
415 }
416 };
417 });
418
419 Ok((
420 rx,
421 CommandChild {
422 inner: child,
423 stdin_writer,
424 #[cfg(unix)]
425 process_group,
426 #[cfg(windows)]
427 job,
428 },
429 ))
430 }
431
432 pub async fn status(self) -> crate::Result<ExitStatus> {
446 let (mut rx, _child) = self.spawn()?;
447 let mut code = None;
448 #[allow(clippy::collapsible_match)]
449 while let Some(event) = rx.recv().await {
450 if let CommandEvent::Terminated(payload) = event {
451 code = payload.code;
452 }
453 }
454 Ok(ExitStatus { code })
455 }
456
457 pub async fn output(self) -> crate::Result<Output> {
473 let (mut rx, _child) = self.spawn()?;
474
475 let mut code = None;
476 let mut stdout = Vec::new();
477 let mut stderr = Vec::new();
478
479 while let Some(event) = rx.recv().await {
480 match event {
481 CommandEvent::Terminated(payload) => {
482 code = payload.code;
483 }
484 CommandEvent::Stdout(line) => {
485 stdout.extend(line);
486 stdout.push(NEWLINE_BYTE);
487 }
488 CommandEvent::Stderr(line) => {
489 stderr.extend(line);
490 stderr.push(NEWLINE_BYTE);
491 }
492 CommandEvent::Error(_) => {}
493 }
494 }
495 Ok(Output {
496 status: ExitStatus { code },
497 stdout,
498 stderr,
499 })
500 }
501}
502
503fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
504 mut reader: BufReader<PipeReader>,
505 tx: Sender<CommandEvent>,
506 wrapper: F,
507) {
508 loop {
509 let result = reader.fill_buf();
510 match result {
511 Ok(buf) => {
512 let length = buf.len();
513 if length == 0 {
514 break;
515 }
516 let tx_ = tx.clone();
517 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
518 reader.consume(length);
519 }
520 Err(e) => {
521 let tx_ = tx.clone();
522 let _ = block_on_task(
523 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
524 );
525 }
526 }
527 }
528}
529
530fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
531 mut reader: BufReader<PipeReader>,
532 tx: Sender<CommandEvent>,
533 wrapper: F,
534) {
535 loop {
536 let mut buf = Vec::new();
537 match tauri::utils::io::read_line(&mut reader, &mut buf) {
538 Ok(n) => {
539 if n == 0 {
540 break;
541 }
542 let tx_ = tx.clone();
543 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
544 }
545 Err(e) => {
546 let _ =
547 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await });
548 break;
549 }
550 }
551 }
552}
553
554fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
555 tx: Sender<CommandEvent>,
556 pipe_reader: PipeReader,
557 wrapper: F,
558 raw_out: bool,
559) -> JoinHandle<()> {
560 spawn(move || {
561 let reader = BufReader::new(pipe_reader);
562
563 if raw_out {
564 read_raw_bytes(reader, tx, wrapper);
565 } else {
566 read_line(reader, tx, wrapper);
567 }
568 })
569}
570
571#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn relative_command_path_resolves() {
578 let cwd_parent = platform::current_exe()
579 .unwrap()
580 .parent()
581 .unwrap()
582 .parent() .unwrap()
584 .to_owned();
585 assert_eq!(
586 relative_command_path(Path::new("Tauri.Example")).unwrap(),
587 cwd_parent.join(if cfg!(windows) {
588 "Tauri.Example.exe"
589 } else {
590 "Tauri.Example"
591 })
592 );
593 assert_eq!(
594 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
595 cwd_parent.join(if cfg!(windows) {
596 "Tauri.Example.exe"
597 } else {
598 "Tauri.Example"
599 })
600 );
601 }
602
603 #[cfg(not(windows))]
604 #[test]
605 fn test_cmd_spawn_output() {
606 let cmd = Command::new("cat").args(["test/test.txt"]);
607 let (mut rx, _) = cmd.spawn().unwrap();
608
609 tauri::async_runtime::block_on(async move {
610 while let Some(event) = rx.recv().await {
611 match event {
612 CommandEvent::Terminated(payload) => {
613 assert_eq!(payload.code, Some(0));
614 }
615 CommandEvent::Stdout(line) => {
616 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
617 }
618 _ => {}
619 }
620 }
621 });
622 }
623
624 #[cfg(not(windows))]
625 #[test]
626 fn test_cmd_spawn_raw_output() {
627 let cmd = Command::new("cat").args(["test/test.txt"]);
628 let (mut rx, _) = cmd.spawn().unwrap();
629
630 tauri::async_runtime::block_on(async move {
631 while let Some(event) = rx.recv().await {
632 match event {
633 CommandEvent::Terminated(payload) => {
634 assert_eq!(payload.code, Some(0));
635 }
636 CommandEvent::Stdout(line) => {
637 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
638 }
639 _ => {}
640 }
641 }
642 });
643 }
644
645 #[cfg(not(windows))]
646 #[test]
647 fn test_cmd_spawn_fail() {
649 let cmd = Command::new("cat").args(["test/"]);
650 let (mut rx, _) = cmd.spawn().unwrap();
651
652 tauri::async_runtime::block_on(async move {
653 while let Some(event) = rx.recv().await {
654 match event {
655 CommandEvent::Terminated(payload) => {
656 assert_eq!(payload.code, Some(1));
657 }
658 CommandEvent::Stderr(line) => {
659 assert_eq!(
660 String::from_utf8(line).unwrap(),
661 "cat: test/: Is a directory\n"
662 );
663 }
664 _ => {}
665 }
666 }
667 });
668 }
669
670 #[cfg(not(windows))]
671 #[test]
672 fn test_cmd_spawn_raw_fail() {
674 let cmd = Command::new("cat").args(["test/"]);
675 let (mut rx, _) = cmd.spawn().unwrap();
676
677 tauri::async_runtime::block_on(async move {
678 while let Some(event) = rx.recv().await {
679 match event {
680 CommandEvent::Terminated(payload) => {
681 assert_eq!(payload.code, Some(1));
682 }
683 CommandEvent::Stderr(line) => {
684 assert_eq!(
685 String::from_utf8(line).unwrap(),
686 "cat: test/: Is a directory\n"
687 );
688 }
689 _ => {}
690 }
691 }
692 });
693 }
694
695 #[cfg(not(windows))]
696 #[test]
697 fn test_cmd_output_output() {
698 let cmd = Command::new("cat").args(["test/test.txt"]);
699 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
700
701 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
702 assert_eq!(
703 String::from_utf8(output.stdout).unwrap(),
704 "This is a test doc!\n"
705 );
706 }
707
708 #[cfg(not(windows))]
709 #[test]
710 fn test_cmd_output_output_fail() {
711 let cmd = Command::new("cat").args(["test/"]);
712 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
713
714 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
715 assert_eq!(
716 String::from_utf8(output.stderr).unwrap(),
717 "cat: test/: Is a directory\n\n"
718 );
719 }
720
721 #[cfg(not(windows))]
722 #[test]
723 fn test_cmd_spawn_process_group_output() {
724 let cmd = Command::new("cat")
725 .args(["test/test.txt"])
726 .set_process_group(true);
727 let (mut rx, _) = cmd.spawn().unwrap();
728
729 tauri::async_runtime::block_on(async move {
730 while let Some(event) = rx.recv().await {
731 match event {
732 CommandEvent::Terminated(payload) => {
733 assert_eq!(payload.code, Some(0));
734 }
735 CommandEvent::Stdout(line) => {
736 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
737 }
738 _ => {}
739 }
740 }
741 });
742 }
743
744 #[cfg(not(windows))]
745 #[test]
746 fn test_cmd_process_group_kill() {
747 let cmd = Command::new("sh")
750 .args(["-c", "sleep 60"])
751 .set_process_group(true);
752 let (mut rx, child) = cmd.spawn().unwrap();
753 let pid = child.pid();
754
755 let ret = unsafe { libc::kill(pid as i32, 0) };
757 assert_eq!(ret, 0, "process should be running");
758
759 child.kill().unwrap();
761
762 tauri::async_runtime::block_on(async move {
763 while let Some(event) = rx.recv().await {
764 if let CommandEvent::Terminated(payload) = event {
765 assert!(payload.code.is_none() || payload.signal.is_some());
767 break;
768 }
769 }
770 });
771
772 let ret = unsafe { libc::killpg(pid as i32, 0) };
774 assert_ne!(ret, 0, "process group should no longer exist");
775 }
776
777 #[test]
778 fn test_cmd_process_group_output() {
779 #[cfg(not(windows))]
781 let cmd = Command::new("cat").args(["test/test.txt"]);
782 #[cfg(windows)]
783 let cmd = Command::new("cmd").args(["/C", "type test\\test.txt"]);
784
785 let output = tauri::async_runtime::block_on(cmd.set_process_group(true).output()).unwrap();
786
787 assert!(output.status.success());
788 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
789 assert_eq!(
790 String::from_utf8(output.stdout).unwrap().trim(),
791 "This is a test doc!"
792 );
793 }
794
795 #[test]
796 fn test_cmd_kill_after_exit() {
797 for process_group in [false, true] {
798 #[cfg(not(windows))]
799 let cmd = Command::new("true");
800 #[cfg(windows)]
801 let cmd = Command::new("cmd").args(["/C", "exit 0"]);
802 let (rx, child) = cmd.set_process_group(process_group).spawn().unwrap();
803 wait_for_terminated(rx);
804 child.kill().unwrap();
805 }
806 }
807
808 fn sim_command(script: &str) -> Command {
810 if cfg!(windows) {
811 Command::new("powershell").args([
812 "-NoProfile".to_string(),
813 "-ExecutionPolicy".to_string(),
814 "Bypass".to_string(),
815 "-File".to_string(),
816 format!("test/{script}.ps1"),
817 ])
818 } else {
819 Command::new("sh").args([format!("test/{script}.sh")])
820 }
821 }
822
823 fn read_grandchild_pid(rx: &mut Receiver<CommandEvent>) -> u32 {
825 tauri::async_runtime::block_on(async {
826 let mut pid = None;
827 while let Some(event) = rx.recv().await {
828 if let CommandEvent::Stdout(line) = &event {
829 let line_str = String::from_utf8_lossy(line);
830 if let Some(rest) = line_str.strip_prefix("CHILD_PID=") {
831 pid = rest.trim().parse::<u32>().ok();
832 }
833 }
834 if pid.is_some() {
835 break;
836 }
837 }
838 pid.expect("should have received CHILD_PID from script")
839 })
840 }
841
842 fn wait_for_terminated(mut rx: Receiver<CommandEvent>) {
844 let got = tauri::async_runtime::block_on(async move {
845 while let Some(event) = rx.recv().await {
846 if let CommandEvent::Terminated(_) = event {
847 return true;
848 }
849 }
850 false
851 });
852 assert!(got, "expected a Terminated event");
853 }
854
855 #[cfg(not(windows))]
856 fn pid_alive(pid: u32) -> bool {
857 unsafe { libc::kill(pid as i32, 0) == 0 }
858 }
859
860 #[cfg(windows)]
861 fn pid_alive(pid: u32) -> bool {
862 use windows_sys::Win32::{
863 Foundation::{CloseHandle, STILL_ACTIVE},
864 System::Threading::{
865 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
866 },
867 };
868 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
869 if handle.is_null() {
870 return false;
871 }
872 let mut code = 0u32;
873 let alive =
874 unsafe { GetExitCodeProcess(handle, &mut code) } != 0 && code == STILL_ACTIVE as u32;
875 unsafe { CloseHandle(handle) };
876 alive
877 }
878
879 #[cfg(not(windows))]
880 fn force_kill(pid: u32) {
881 unsafe { libc::kill(pid as i32, libc::SIGKILL) };
882 }
883
884 #[cfg(windows)]
885 fn force_kill(pid: u32) {
886 use windows_sys::Win32::{
887 Foundation::CloseHandle,
888 System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess},
889 };
890 let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
891 if !handle.is_null() {
892 unsafe { TerminateProcess(handle, 1) };
893 unsafe { CloseHandle(handle) };
894 }
895 }
896
897 #[test]
904 fn test_pyinstaller_simulation_without_process_group() {
905 let (mut rx, child) = sim_command("pyinstaller_sim").spawn().unwrap();
907
908 let grandchild_pid = read_grandchild_pid(&mut rx);
909 assert!(
910 pid_alive(grandchild_pid),
911 "grandchild should be running before kill"
912 );
913
914 child.kill().unwrap();
916 std::thread::sleep(std::time::Duration::from_millis(100));
917
918 assert!(
920 pid_alive(grandchild_pid),
921 "grandchild should survive when process_group is off"
922 );
923
924 force_kill(grandchild_pid);
927 wait_for_terminated(rx);
928 }
929
930 #[test]
931 fn test_pyinstaller_simulation_with_process_group() {
932 let (mut rx, child) = sim_command("pyinstaller_sim")
934 .set_process_group(true)
935 .spawn()
936 .unwrap();
937
938 let grandchild_pid = read_grandchild_pid(&mut rx);
939 assert!(
940 pid_alive(grandchild_pid),
941 "grandchild should be running before kill"
942 );
943
944 child.kill().unwrap();
946 std::thread::sleep(std::time::Duration::from_millis(100));
947
948 assert!(
950 !pid_alive(grandchild_pid),
951 "grandchild should be killed when process_group is on"
952 );
953
954 wait_for_terminated(rx);
955 }
956
957 #[test]
960 fn test_process_group_kill_after_leader_exit() {
961 let (mut rx, child) = sim_command("launcher_sim")
962 .set_process_group(true)
963 .spawn()
964 .unwrap();
965
966 let grandchild_pid = read_grandchild_pid(&mut rx);
967
968 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
970 while child.inner.try_wait().unwrap().is_none() {
971 assert!(
972 std::time::Instant::now() < deadline,
973 "launcher should exit on its own"
974 );
975 std::thread::sleep(std::time::Duration::from_millis(50));
976 }
977 assert!(
978 pid_alive(grandchild_pid),
979 "grandchild should outlive the launcher"
980 );
981
982 child.kill().unwrap();
983 std::thread::sleep(std::time::Duration::from_millis(100));
984
985 let alive = pid_alive(grandchild_pid);
986 if alive {
987 force_kill(grandchild_pid);
988 }
989 assert!(
990 !alive,
991 "grandchild should be killed even though the launcher already exited"
992 );
993 }
994}