// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ ffi::OsStr, io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::{Command as StdCommand, Stdio}, sync::Arc, thread::{JoinHandle, spawn}, }; #[cfg(unix)] use std::os::unix::process::{CommandExt, ExitStatusExt}; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(windows)] use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED}; const NEWLINE_BYTE: u8 = b'\n'; use tauri::async_runtime::{Receiver, Sender, block_on as block_on_task, channel}; pub use encoding_rs::Encoding; use os_pipe::{PipeReader, PipeWriter, pipe}; use serde::Serialize; use shared_child::SharedChild; use tauri::utils::platform; #[cfg(windows)] mod job_object; /// Payload for the [`CommandEvent::Terminated`] command event. #[derive(Debug, Clone, Serialize)] pub struct TerminatedPayload { /// Exit code of the process. pub code: Option, /// If the process was terminated by a signal, represents that signal. pub signal: Option, } /// A event sent to the command callback. #[derive(Debug, Clone)] #[non_exhaustive] pub enum CommandEvent { /// If configured for raw output, all bytes written to stderr. /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found. Stderr(Vec), /// If configured for raw output, all bytes written to stdout. /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found. Stdout(Vec), /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to a UTF-8 string. Error(String), /// Command process terminated. Terminated(TerminatedPayload), } /// The type to spawn commands. #[derive(Debug)] pub struct Command { cmd: StdCommand, raw_out: bool, process_group: bool, kill_on_drop: bool, } /// Spawned child process. #[derive(Debug)] pub struct CommandChild { inner: Arc, stdin_writer: PipeWriter, #[cfg(unix)] process_group: bool, #[cfg(windows)] job: Option, kill_on_drop: bool, } impl CommandChild { /// Writes to process stdin. pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> { self.stdin_writer.write_all(buf)?; Ok(()) } /// Sends a kill signal to the child and waits for it to exit. /// With `process_group` enabled this kills the whole process group (POSIX) or job object (Windows). pub fn kill(mut self) -> crate::Result<()> { self.send_kill()?; // Already killed, so `Drop` must not do it again. self.kill_on_drop = false; self.inner.wait()?; Ok(()) } /// Sends the kill signal without waiting for the child to exit. fn send_kill(&self) -> crate::Result<()> { // No early return when the child already exited: the rest of its // process group or job object may still be running. #[cfg(unix)] if self.process_group { // The pgid can't be reused while the group has members, so this is // safe even after the group leader was reaped. let pgid = self.inner.id() as libc::pid_t; if unsafe { libc::killpg(pgid, libc::SIGKILL) } != 0 { let err = std::io::Error::last_os_error(); // ESRCH: every process in the group already exited. if err.raw_os_error() != Some(libc::ESRCH) { return Err(err.into()); } } } else { self.inner.kill()?; } #[cfg(windows)] match &self.job { Some(job) => job.terminate()?, None => self.inner.kill()?, } Ok(()) } /// Returns the process pid. pub fn pid(&self) -> u32 { self.inner.id() } } impl Drop for CommandChild { /// Kills the child process (or its process group / job object) if `kill_on_drop` is enabled. fn drop(&mut self) { if self.kill_on_drop && let Err(e) = self.send_kill() { log::error!("Failed to kill child process {}: {}", self.inner.id(), e); } } } /// Describes the result of a process after it has terminated. #[derive(Debug)] pub struct ExitStatus { // This field is intentionally left private. // See: https://github.com/tauri-apps/plugins-workspace/pull/3115. code: Option, } impl ExitStatus { /// Returns the exit code of the process, if any. pub fn code(&self) -> Option { self.code } /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status. pub fn success(&self) -> bool { self.code == Some(0) } } /// The output of a finished process. #[derive(Debug)] pub struct Output { /// The status (exit code) of the process. pub status: ExitStatus, /// The data that the process wrote to stdout. pub stdout: Vec, /// The data that the process wrote to stderr. pub stderr: Vec, } fn relative_command_path(command: &Path) -> crate::Result { let exe_path = platform::current_exe()?; let exe_dir = exe_path .parent() .ok_or(crate::Error::CurrentExeHasNoParent)?; // If a test is being run, the executable is in the "deps" directory, so we need to go up one level. let base_dir = if exe_dir.ends_with("deps") { exe_dir.parent().unwrap_or(exe_dir) } else { exe_dir }; let mut command_path = base_dir.join(command); #[cfg(windows)] { let already_exe = command_path.extension().is_some_and(|ext| ext == "exe"); if !already_exe { // do not use with_extension to retain dots in the command filename command_path.as_mut_os_string().push(".exe"); } } #[cfg(not(windows))] { if command_path.extension().is_some_and(|ext| ext == "exe") { command_path.set_extension(""); } } Ok(command_path) } impl From for StdCommand { fn from(cmd: Command) -> StdCommand { cmd.cmd } } impl Command { pub(crate) fn new>(program: S) -> Self { log::debug!( "Creating sidecar {}", program.as_ref().to_str().unwrap_or("") ); let mut command = StdCommand::new(program); command.stdout(Stdio::piped()); command.stdin(Stdio::piped()); command.stderr(Stdio::piped()); #[cfg(windows)] command.creation_flags(CREATE_NO_WINDOW); Self { cmd: command, raw_out: false, process_group: false, kill_on_drop: false, } } pub(crate) fn new_sidecar>(program: S) -> crate::Result { Ok(Self::new(relative_command_path(program.as_ref())?)) } /// Appends an argument to the command. #[must_use] pub fn arg>(mut self, arg: S) -> Self { self.cmd.arg(arg); self } /// Appends arguments to the command. #[must_use] pub fn args(mut self, args: I) -> Self where I: IntoIterator, S: AsRef, { self.cmd.args(args); self } /// Clears the entire environment map for the child process. #[must_use] pub fn env_clear(mut self) -> Self { self.cmd.env_clear(); self } /// Inserts or updates an explicit environment variable mapping. #[must_use] pub fn env(mut self, key: K, value: V) -> Self where K: AsRef, V: AsRef, { self.cmd.env(key, value); self } /// Adds or updates multiple environment variable mappings. #[must_use] pub fn envs(mut self, envs: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { self.cmd.envs(envs); self } /// Sets the working directory for the child process. #[must_use] pub fn current_dir>(mut self, current_dir: P) -> Self { self.cmd.current_dir(current_dir); self } /// Configures the reader to output bytes from the child process exactly as received pub fn set_raw_out(mut self, raw_out: bool) -> Self { self.raw_out = raw_out; self } /// Configures the command to spawn in a new process group (POSIX) or job object (Windows). /// /// When enabled, killing the child process will also kill all processes in the group, /// which is useful for programs that spawn child processes (e.g. PyInstaller wrappers). #[must_use] pub fn set_process_group(mut self, process_group: bool) -> Self { self.process_group = process_group; self } /// Configures whether the child process is killed when its [`CommandChild`] is dropped. /// Defaults to `false`, so a dropped [`CommandChild`] leaves the process running. /// /// The kill works like [`CommandChild::kill`] but does not wait for the process to exit. /// Without [`Command::set_process_group`] only the child itself is killed, and any /// processes it spawned keep running; enable it to kill the whole process group (POSIX) /// or job object (Windows). /// /// This has no effect on processes spawned from the JavaScript API; the plugin kills those when the app exits. #[must_use] pub fn kill_on_drop(mut self, kill_on_drop: bool) -> Self { self.kill_on_drop = kill_on_drop; self } /// Spawns the command. /// /// # Examples /// /// ```rust,no_run /// use tauri_plugin_shell::{process::CommandEvent, ShellExt}; /// tauri::Builder::default() /// .setup(|app| { /// let handle = app.handle().clone(); /// tauri::async_runtime::spawn(async move { /// let (mut rx, mut child) = handle /// .shell() /// .command("cargo") /// .args(["tauri", "dev"]) /// .spawn() /// .expect("Failed to spawn cargo"); /// /// let mut i = 0; /// while let Some(event) = rx.recv().await { /// if let CommandEvent::Stdout(line) = event { /// println!("got: {}", String::from_utf8(line).unwrap()); /// i += 1; /// if i == 4 { /// child.write("message from Rust\n".as_bytes()).unwrap(); /// i = 0; /// } /// } /// } /// }); /// Ok(()) /// }); /// ``` /// /// Depending on the command you spawn, it might output in a specific encoding, to parse the output lines in this case: /// /// ```rust,no_run /// use tauri_plugin_shell::{process::{CommandEvent, Encoding}, ShellExt}; /// tauri::Builder::default() /// .setup(|app| { /// let handle = app.handle().clone(); /// tauri::async_runtime::spawn(async move { /// let (mut rx, mut child) = handle /// .shell() /// .command("some-program") /// .arg("some-arg") /// .spawn() /// .expect("Failed to spawn some-program"); /// /// let encoding = Encoding::for_label(b"windows-1252").unwrap(); /// while let Some(event) = rx.recv().await { /// if let CommandEvent::Stdout(line) = event { /// let (decoded, _, _) = encoding.decode(&line); /// println!("got: {decoded}"); /// } /// } /// }); /// Ok(()) /// }); /// ``` pub fn spawn(self) -> crate::Result<(Receiver, CommandChild)> { let raw = self.raw_out; let process_group = self.process_group; let kill_on_drop = self.kill_on_drop; let mut command: StdCommand = self.into(); let (stdout_reader, stdout_writer) = pipe()?; let (stderr_reader, stderr_writer) = pipe()?; let (stdin_reader, stdin_writer) = pipe()?; command.stdout(stdout_writer); command.stderr(stderr_writer); command.stdin(stdin_reader); #[cfg(unix)] if process_group { command.process_group(0); } #[cfg(windows)] if process_group { // Start suspended so nothing can be spawned before the child is in the job. command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); } let child = command.spawn()?; #[cfg(windows)] let job = if process_group { match job_object::JobObject::assign(&child) { Ok(job) => Some(job), Err(e) => { // Don't leave the suspended child behind. let mut child = child; let _ = child.kill(); let _ = child.wait(); return Err(e.into()); } } } else { None }; let child = Arc::new(SharedChild::new(child)?); let child_ = child.clone(); let (tx, rx) = channel(1); let stdout_thread = spawn_pipe_reader(tx.clone(), stdout_reader, CommandEvent::Stdout, raw); let stderr_thread = spawn_pipe_reader(tx.clone(), stderr_reader, CommandEvent::Stderr, raw); spawn(move || { let status = child_.wait(); // `Terminated` must be the last event: wait for the readers to drain the pipes, // since a short-lived child can exit before they have read anything. let _ = stdout_thread.join(); let _ = stderr_thread.join(); let _ = match status { Ok(status) => block_on_task(async move { tx.send(CommandEvent::Terminated(TerminatedPayload { code: status.code(), #[cfg(windows)] signal: None, #[cfg(unix)] signal: status.signal(), })) .await }), Err(e) => { block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }) } }; }); Ok(( rx, CommandChild { inner: child, stdin_writer, #[cfg(unix)] process_group, #[cfg(windows)] job, kill_on_drop, }, )) } /// Executes a command as a child process, waiting for it to finish and collecting its exit status. /// Stdin, stdout and stderr are ignored. /// /// # Examples /// ```rust,no_run /// use tauri_plugin_shell::ShellExt; /// tauri::Builder::default() /// .setup(|app| { /// let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() }); /// println!("`which` finished with status: {:?}", status.code()); /// Ok(()) /// }); /// ``` pub async fn status(self) -> crate::Result { let (mut rx, _child) = self.spawn()?; let mut code = None; #[allow(clippy::collapsible_match)] while let Some(event) = rx.recv().await { if let CommandEvent::Terminated(payload) = event { code = payload.code; } } Ok(ExitStatus { code }) } /// Executes the command as a child process, waiting for it to finish and collecting all of its output. /// Stdin is ignored. /// /// # Examples /// /// ```rust,no_run /// use tauri_plugin_shell::ShellExt; /// tauri::Builder::default() /// .setup(|app| { /// let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() }); /// assert!(output.status.success()); /// assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI"); /// Ok(()) /// }); /// ``` pub async fn output(self) -> crate::Result { let (mut rx, _child) = self.spawn()?; let mut code = None; let mut stdout = Vec::new(); let mut stderr = Vec::new(); while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { code = payload.code; } CommandEvent::Stdout(line) => { stdout.extend(line); stdout.push(NEWLINE_BYTE); } CommandEvent::Stderr(line) => { stderr.extend(line); stderr.push(NEWLINE_BYTE); } CommandEvent::Error(_) => {} } } Ok(Output { status: ExitStatus { code }, stdout, stderr, }) } } fn read_raw_bytes) -> CommandEvent + Send + Copy + 'static>( mut reader: BufReader, tx: Sender, wrapper: F, ) { loop { let result = reader.fill_buf(); match result { Ok(buf) => { let length = buf.len(); if length == 0 { break; } let tx_ = tx.clone(); let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await }); reader.consume(length); } Err(e) => { let tx_ = tx.clone(); let _ = block_on_task( async move { tx_.send(CommandEvent::Error(e.to_string())).await }, ); } } } } fn read_line) -> CommandEvent + Send + Copy + 'static>( mut reader: BufReader, tx: Sender, wrapper: F, ) { loop { let mut buf = Vec::new(); match tauri::utils::io::read_line(&mut reader, &mut buf) { Ok(n) => { if n == 0 { break; } let tx_ = tx.clone(); let _ = block_on_task(async move { tx_.send(wrapper(buf)).await }); } Err(e) => { let _ = block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }); break; } } } } fn spawn_pipe_reader) -> CommandEvent + Send + Copy + 'static>( tx: Sender, pipe_reader: PipeReader, wrapper: F, raw_out: bool, ) -> JoinHandle<()> { spawn(move || { let reader = BufReader::new(pipe_reader); if raw_out { read_raw_bytes(reader, tx, wrapper); } else { read_line(reader, tx, wrapper); } }) } // tests for the commands functions. #[cfg(test)] mod tests { use super::*; #[test] fn relative_command_path_resolves() { let cwd_parent = platform::current_exe() .unwrap() .parent() .unwrap() .parent() // Go up once more to get out of the "deps" directory .unwrap() .to_owned(); assert_eq!( relative_command_path(Path::new("Tauri.Example")).unwrap(), cwd_parent.join(if cfg!(windows) { "Tauri.Example.exe" } else { "Tauri.Example" }) ); assert_eq!( relative_command_path(Path::new("Tauri.Example.exe")).unwrap(), cwd_parent.join(if cfg!(windows) { "Tauri.Example.exe" } else { "Tauri.Example" }) ); } #[cfg(not(windows))] #[test] fn test_cmd_spawn_output() { let cmd = Command::new("cat").args(["test/test.txt"]); let (mut rx, _) = cmd.spawn().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { assert_eq!(payload.code, Some(0)); } CommandEvent::Stdout(line) => { assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!"); } _ => {} } } }); } #[cfg(not(windows))] #[test] fn test_cmd_spawn_raw_output() { let cmd = Command::new("cat").args(["test/test.txt"]); let (mut rx, _) = cmd.spawn().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { assert_eq!(payload.code, Some(0)); } CommandEvent::Stdout(line) => { assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!"); } _ => {} } } }); } #[cfg(not(windows))] #[test] // test the failure case fn test_cmd_spawn_fail() { let cmd = Command::new("cat").args(["test/"]); let (mut rx, _) = cmd.spawn().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { assert_eq!(payload.code, Some(1)); } CommandEvent::Stderr(line) => { assert_eq!( String::from_utf8(line).unwrap(), "cat: test/: Is a directory\n" ); } _ => {} } } }); } #[cfg(not(windows))] #[test] // test the failure case (raw encoding) fn test_cmd_spawn_raw_fail() { let cmd = Command::new("cat").args(["test/"]); let (mut rx, _) = cmd.spawn().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { assert_eq!(payload.code, Some(1)); } CommandEvent::Stderr(line) => { assert_eq!( String::from_utf8(line).unwrap(), "cat: test/: Is a directory\n" ); } _ => {} } } }); } #[cfg(not(windows))] #[test] fn test_cmd_output_output() { let cmd = Command::new("cat").args(["test/test.txt"]); let output = tauri::async_runtime::block_on(cmd.output()).unwrap(); assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); assert_eq!( String::from_utf8(output.stdout).unwrap(), "This is a test doc!\n" ); } #[cfg(not(windows))] #[test] fn test_cmd_output_output_fail() { let cmd = Command::new("cat").args(["test/"]); let output = tauri::async_runtime::block_on(cmd.output()).unwrap(); assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); assert_eq!( String::from_utf8(output.stderr).unwrap(), "cat: test/: Is a directory\n\n" ); } #[cfg(not(windows))] #[test] fn test_cmd_spawn_process_group_output() { let cmd = Command::new("cat") .args(["test/test.txt"]) .set_process_group(true); let (mut rx, _) = cmd.spawn().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { assert_eq!(payload.code, Some(0)); } CommandEvent::Stdout(line) => { assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!"); } _ => {} } } }); } #[cfg(not(windows))] #[test] fn test_cmd_process_group_kill() { // Spawn a shell that runs a sleep command as a child process. // With process_group enabled, killing the parent should also kill the child. let cmd = Command::new("sh") .args(["-c", "sleep 60"]) .set_process_group(true); let (mut rx, child) = cmd.spawn().unwrap(); let pid = child.pid(); // Verify the process is running let ret = unsafe { libc::kill(pid as i32, 0) }; assert_eq!(ret, 0, "process should be running"); // Kill the process group child.kill().unwrap(); tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { if let CommandEvent::Terminated(payload) = event { // Process was killed by signal, so code is None and signal is Some assert!(payload.code.is_none() || payload.signal.is_some()); break; } } }); // Verify the process group is gone let ret = unsafe { libc::killpg(pid as i32, 0) }; assert_ne!(ret, 0, "process group should no longer exist"); } #[cfg(not(windows))] fn wait_terminated(mut rx: Receiver) -> TerminatedPayload { tauri::async_runtime::block_on(async move { while let Some(event) = rx.recv().await { if let CommandEvent::Terminated(payload) = event { return payload; } } panic!("channel closed without a Terminated event"); }) } #[cfg(not(windows))] #[test] fn test_cmd_kill_on_drop() { let (rx, child) = Command::new("sleep") .args(["60"]) .kill_on_drop(true) .spawn() .unwrap(); drop(child); let payload = wait_terminated(rx); assert_eq!(payload.signal, Some(libc::SIGKILL)); } #[cfg(not(windows))] #[test] fn test_cmd_no_kill_on_drop_by_default() { let (_rx, child) = Command::new("sleep").args(["60"]).spawn().unwrap(); let pid = child.pid(); drop(child); std::thread::sleep(std::time::Duration::from_millis(200)); let ret = unsafe { libc::kill(pid as i32, 0) }; assert_eq!(ret, 0, "process should still be running"); unsafe { libc::kill(pid as i32, libc::SIGKILL) }; } #[cfg(not(windows))] #[test] fn test_cmd_kill_on_drop_process_group() { // The shell's own child (sleep) must die too, not just the shell. let (rx, child) = Command::new("sh") .args(["-c", "sleep 60; true"]) .set_process_group(true) .kill_on_drop(true) .spawn() .unwrap(); let pid = child.pid(); drop(child); wait_terminated(rx); let ret = unsafe { libc::killpg(pid as i32, 0) }; assert_ne!(ret, 0, "process group should no longer exist"); } #[test] fn test_cmd_process_group_output() { // Hangs on Windows if the suspended child is never resumed. #[cfg(not(windows))] let cmd = Command::new("cat").args(["test/test.txt"]); #[cfg(windows)] let cmd = Command::new("cmd").args(["/C", "type test\\test.txt"]); let output = tauri::async_runtime::block_on(cmd.set_process_group(true).output()).unwrap(); assert!(output.status.success()); assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); assert_eq!( String::from_utf8(output.stdout).unwrap().trim(), "This is a test doc!" ); } #[test] fn test_cmd_kill_after_exit() { for process_group in [false, true] { #[cfg(not(windows))] let cmd = Command::new("true"); #[cfg(windows)] let cmd = Command::new("cmd").args(["/C", "exit 0"]); let (rx, child) = cmd.set_process_group(process_group).spawn().unwrap(); wait_for_terminated(rx); child.kill().unwrap(); } } /// Runs `test/