You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tauri-plugins-workspace/plugins/shell/src/process/mod.rs

518 lines
15 KiB

// 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<i32>,
/// If the process was terminated by a signal, represents that signal.
pub signal: Option<i32>,
}
/// 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<u8>),
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
Stdout(Vec<u8>),
/// 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<SharedChild>,
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<i32>,
}
impl ExitStatus {
/// Returns the exit code of the process, if any.
pub fn code(&self) -> Option<i32> {
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<u8>,
/// The data that the process wrote to stderr.
pub stderr: Vec<u8>,
}
fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
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<Command> for StdCommand {
fn from(cmd: Command) -> StdCommand {
cmd.0
}
}
impl Command {
pub(crate) fn new<S: AsRef<OsStr>>(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<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
Ok(Self::new(relative_command_path(program.as_ref())?))
}
/// Appends an argument to the command.
#[must_use]
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.0.arg(arg);
self
}
/// Appends arguments to the command.
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
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<K, V>(mut self, key: K, value: V) -> Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.env(key, value);
self
}
/// Adds or updates multiple environment variable mappings.
#[must_use]
pub fn envs<I, K, V>(mut self, envs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(envs);
self
}
/// Sets the working directory for the child process.
#[must_use]
pub fn current_dir<P: AsRef<Path>>(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<CommandEvent>, 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<ExitStatus> {
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<Output> {
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<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
tx: Sender<CommandEvent>,
guard: Arc<RwLock<()>>,
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"
);
}
}