// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ ffi::OsStr, io::{BufReader, Write}, path::{Path, PathBuf}, process::{Command as StdCommand, Stdio}, sync::{Arc, RwLock}, thread::spawn, }; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(windows)] const CREATE_NO_WINDOW: u32 = 0x0800_0000; const NEWLINE_BYTE: u8 = b'\n'; use tauri::async_runtime::{block_on as block_on_task, channel, Receiver, Sender}; pub use encoding_rs::Encoding; use os_pipe::{pipe, PipeReader, PipeWriter}; use serde::Serialize; use shared_child::SharedChild; use tauri::utils::platform; /// 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 { /// Stderr bytes until a newline (\n) or carriage return (\r) is found. Stderr(Vec), /// Stdout 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 an UTF-8 string. Error(String), /// Command process terminated. Terminated(TerminatedPayload), } /// The type to spawn commands. #[derive(Debug)] pub struct Command(StdCommand); /// Spawned child process. #[derive(Debug)] pub struct CommandChild { inner: Arc, stdin_writer: PipeWriter, } 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. pub fn kill(self) -> crate::Result<()> { self.inner.kill()?; Ok(()) } /// Returns the process pid. pub fn pid(&self) -> u32 { self.inner.id() } } /// Describes the result of a process after it has terminated. #[derive(Debug)] pub struct ExitStatus { 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 { match platform::current_exe()?.parent() { #[cfg(windows)] Some(exe_dir) => Ok(exe_dir.join(command).with_extension("exe")), #[cfg(not(windows))] Some(exe_dir) => Ok(exe_dir.join(command)), None => Err(crate::Error::CurrentExeHasNoParent), } } impl From for StdCommand { fn from(cmd: Command) -> StdCommand { cmd.0 } } impl Command { pub(crate) fn new>(program: S) -> Self { 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(command) } 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.0.arg(arg); self } /// Appends arguments to the command. #[must_use] pub fn args(mut self, args: I) -> Self where I: IntoIterator, S: AsRef, { self.0.args(args); self } /// Clears the entire environment map for the child process. #[must_use] pub fn env_clear(mut self) -> Self { self.0.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.0.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.0.envs(envs); self } /// Sets the working directory for the child process. #[must_use] pub fn current_dir>(mut self, current_dir: P) -> Self { self.0.current_dir(current_dir); 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(()) /// }); /// ``` pub fn spawn(self) -> crate::Result<(Receiver, CommandChild)> { 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); let shared_child = SharedChild::spawn(&mut command)?; let child = Arc::new(shared_child); let child_ = child.clone(); let guard = Arc::new(RwLock::new(())); let (tx, rx) = channel(1); spawn_pipe_reader( tx.clone(), guard.clone(), stdout_reader, CommandEvent::Stdout, ); spawn_pipe_reader( tx.clone(), guard.clone(), stderr_reader, CommandEvent::Stderr, ); spawn(move || { let _ = match child_.wait() { Ok(status) => { let _l = guard.write().unwrap(); 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) => { let _l = guard.write().unwrap(); block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }) } }; }); Ok(( rx, CommandChild { inner: child, stdin_writer, }, )) } /// 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 spawn_pipe_reader) -> CommandEvent + Send + Copy + 'static>( tx: Sender, guard: Arc>, pipe_reader: PipeReader, wrapper: F, ) { spawn(move || { let _lock = guard.read().unwrap(); let mut reader = BufReader::new(pipe_reader); 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 tx_ = tx.clone(); let _ = block_on_task( async move { tx_.send(CommandEvent::Error(e.to_string())).await }, ); } } } }); } // tests for the commands functions. #[cfg(test)] mod tests { #[cfg(not(windows))] use super::*; #[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" ); } _ => {} } } }); } #[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" ); } _ => {} } } }); } #[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" ); } }