// Copyright 2019-2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::HashMap, env, io::{BufRead, BufReader, Write}, path::PathBuf, process::{exit, Command as StdCommand, Stdio}, sync::Arc, }; #[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; use crate::async_runtime::{channel, spawn, Receiver, RwLock}; use os_pipe::{pipe, PipeWriter}; use serde::Serialize; use shared_child::SharedChild; use tauri_utils::platform; /// Payload for the `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, Serialize)] #[serde(tag = "event", content = "payload")] pub enum CommandEvent { /// Stderr line. Stderr(String), /// Stdout line. Stdout(String), /// An error happened. Error(String), /// Command process terminated. Terminated(TerminatedPayload), } macro_rules! get_std_command { ($self: ident) => {{ let mut command = StdCommand::new($self.program); command.args(&$self.args); command.stdout(Stdio::piped()); command.stdin(Stdio::piped()); command.stderr(Stdio::piped()); if $self.env_clear { command.env_clear(); } command.envs($self.env); if let Some(current_dir) = $self.current_dir { command.current_dir(current_dir); } #[cfg(windows)] command.creation_flags(CREATE_NO_WINDOW); command }}; } /// Get the current binary pub fn current_binary() -> Option { let mut current_binary = None; // if we are running with an APP Image, we should return the app image path #[cfg(target_os = "linux")] if let Some(app_image_path) = env::var_os("APPIMAGE") { current_binary = Some(PathBuf::from(app_image_path)); } // if we didn't extracted binary in previous step, // let use the current_exe from current environment if current_binary.is_none() { if let Ok(current_process) = env::current_exe() { current_binary = Some(current_process); } } current_binary } /// Restart the process. pub fn restart() { if let Some(path) = current_binary() { StdCommand::new(path) .spawn() .expect("application failed to start"); } exit(0); } /// API to spawn commands. pub struct Command { program: String, args: Vec, env_clear: bool, env: HashMap, current_dir: Option, } /// Child spawned. pub struct CommandChild { inner: Arc, stdin_writer: PipeWriter, } impl CommandChild { /// Write to process stdin. pub fn write(&mut self, buf: &[u8]) -> crate::api::Result<()> { self.stdin_writer.write_all(buf)?; Ok(()) } /// Send a kill signal to the child. pub fn kill(self) -> crate::api::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. pub struct ExitStatus { code: Option, } impl ExitStatus { /// Returns the exit code of the process, if any. pub fn code(&self) -> Option { self.code } /// Was termination successful? 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. pub struct Output { /// The status (exit code) of the process. pub status: ExitStatus, /// The data that the process wrote to stdout. pub stdout: String, /// The data that the process wrote to stderr. pub stderr: String, } #[cfg(not(windows))] fn relative_command_path(command: String) -> crate::Result { match std::env::current_exe()?.parent() { Some(exe_dir) => Ok(format!( "{}/{}", exe_dir.to_string_lossy().to_string(), command )), None => Err(super::Error::Command("Could not evaluate executable dir".to_string()).into()), } } #[cfg(windows)] fn relative_command_path(command: String) -> crate::Result { match std::env::current_exe()?.parent() { Some(exe_dir) => Ok(format!( "{}/{}.exe", exe_dir.to_string_lossy().to_string(), command )), None => Err(super::Error::Command("Could not evaluate executable dir".to_string()).into()), } } impl Command { /// Creates a new Command for launching the given program. pub fn new>(program: S) -> Self { Self { program: program.into(), args: Default::default(), env_clear: false, env: Default::default(), current_dir: None, } } /// Creates a new Command for launching the given sidecar program. pub fn new_sidecar>(program: S) -> crate::Result { let program = format!( "{}-{}", program.into(), platform::target_triple().expect("unsupported platform") ); Ok(Self::new(relative_command_path(program)?)) } /// Append args to the command. pub fn args(mut self, args: I) -> Self where I: IntoIterator, S: AsRef, { for arg in args { self.args.push(arg.as_ref().to_string()); } self } /// Clears the entire environment map for the child process. pub fn env_clear(mut self) -> Self { self.env_clear = true; self } /// Adds or updates multiple environment variable mappings. pub fn envs(mut self, env: HashMap) -> Self { self.env = env; self } /// Sets the working directory for the child process. pub fn current_dir(mut self, current_dir: PathBuf) -> Self { self.current_dir.replace(current_dir); self } /// Spawns the command. pub fn spawn(self) -> crate::api::Result<(Receiver, CommandChild)> { let mut command = get_std_command!(self); 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); let tx_ = tx.clone(); let guard_ = guard.clone(); spawn(async move { let _lock = guard_.read().await; let reader = BufReader::new(stdout_reader); for line in reader.lines() { let _ = match line { Ok(line) => tx_.send(CommandEvent::Stdout(line)).await, Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await, }; } }); let tx_ = tx.clone(); let guard_ = guard.clone(); spawn(async move { let _lock = guard_.read().await; let reader = BufReader::new(stderr_reader); for line in reader.lines() { let _ = match line { Ok(line) => tx_.send(CommandEvent::Stderr(line)).await, Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await, }; } }); spawn(async move { let _ = match child_.wait() { Ok(status) => { guard.write().await; tx.send(CommandEvent::Terminated(TerminatedPayload { code: status.code(), #[cfg(windows)] signal: None, #[cfg(unix)] signal: status.signal(), })) .await } Err(e) => { guard.write().await; 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. pub fn status(self) -> crate::api::Result { let (mut rx, _child) = self.spawn()?; let code = crate::async_runtime::block_on(async move { let mut code = None; while let Some(event) = rx.recv().await { if let CommandEvent::Terminated(payload) = event { code = payload.code; } } 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. pub fn output(self) -> crate::api::Result { let (mut rx, _child) = self.spawn()?; let output = crate::async_runtime::block_on(async move { let mut code = None; let mut stdout = String::new(); let mut stderr = String::new(); while let Some(event) = rx.recv().await { match event { CommandEvent::Terminated(payload) => { code = payload.code; } CommandEvent::Stdout(line) => { stdout.push_str(line.as_str()); stdout.push('\n'); } CommandEvent::Stderr(line) => { stderr.push_str(line.as_str()); stderr.push('\n'); } CommandEvent::Error(_) => {} } } Output { status: ExitStatus { code }, stdout, stderr, } }); Ok(output) } } // tests for the commands functions. #[cfg(test)] mod test { use super::*; #[cfg(not(windows))] #[test] fn test_cmd_output() { // create a command to run cat. let cmd = Command::new("cat").args(&["test/api/test.txt"]); let (mut rx, _) = cmd.spawn().unwrap(); crate::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!(line, "This is a test doc!".to_string()); } _ => {} } } }); } #[cfg(not(windows))] #[test] // test the failure case fn test_cmd_fail() { let cmd = Command::new("cat").args(&["test/api/"]); let (mut rx, _) = cmd.spawn().unwrap(); crate::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!(line, "cat: test/api/: Is a directory".to_string()); } _ => {} } } }); } }