use std::{
ffi::OsStr,
io::{BufRead, 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;
#[derive(Debug, Clone, Serialize)]
pub struct TerminatedPayload {
pub code: Option<i32>,
pub signal: Option<i32>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CommandEvent {
Stderr(Vec<u8>),
Stdout(Vec<u8>),
Error(String),
Terminated(TerminatedPayload),
}
#[derive(Debug)]
pub struct Command {
cmd: StdCommand,
raw_out: bool,
}
#[derive(Debug)]
pub struct CommandChild {
inner: Arc<SharedChild>,
stdin_writer: PipeWriter,
}
impl CommandChild {
pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
self.stdin_writer.write_all(buf)?;
Ok(())
}
pub fn kill(self) -> crate::Result<()> {
self.inner.kill()?;
Ok(())
}
pub fn pid(&self) -> u32 {
self.inner.id()
}
}
#[derive(Debug)]
pub struct ExitStatus {
code: Option<i32>,
}
impl ExitStatus {
pub fn code(&self) -> Option<i32> {
self.code
}
pub fn success(&self) -> bool {
self.code == Some(0)
}
}
#[derive(Debug)]
pub struct Output {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
let exe_path = platform::current_exe()?;
let exe_dir = exe_path
.parent()
.ok_or(crate::Error::CurrentExeHasNoParent)?;
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 {
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<Command> for StdCommand {
fn from(cmd: Command) -> StdCommand {
cmd.cmd
}
}
impl Command {
pub(crate) fn new<S: AsRef<OsStr>>(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,
}
}
pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
Ok(Self::new(relative_command_path(program.as_ref())?))
}
#[must_use]
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.cmd.arg(arg);
self
}
#[must_use]
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.cmd.args(args);
self
}
#[must_use]
pub fn env_clear(mut self) -> Self {
self.cmd.env_clear();
self
}
#[must_use]
pub fn env<K, V>(mut self, key: K, value: V) -> Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.cmd.env(key, value);
self
}
#[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.cmd.envs(envs);
self
}
#[must_use]
pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
self.cmd.current_dir(current_dir);
self
}
pub fn set_raw_out(mut self, raw_out: bool) -> Self {
self.raw_out = raw_out;
self
}
pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
let raw = self.raw_out;
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,
raw,
);
spawn_pipe_reader(
tx.clone(),
guard.clone(),
stderr_reader,
CommandEvent::Stderr,
raw,
);
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,
},
))
}
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 })
}
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 read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
mut reader: BufReader<PipeReader>,
tx: Sender<CommandEvent>,
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<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
mut reader: BufReader<PipeReader>,
tx: Sender<CommandEvent>,
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 tx_ = tx.clone();
let _ = block_on_task(
async move { tx_.send(CommandEvent::Error(e.to_string())).await },
);
break;
}
}
}
}
fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
tx: Sender<CommandEvent>,
guard: Arc<RwLock<()>>,
pipe_reader: PipeReader,
wrapper: F,
raw_out: bool,
) {
spawn(move || {
let _lock = guard.read().unwrap();
let reader = BufReader::new(pipe_reader);
if raw_out {
read_raw_bytes(reader, tx, wrapper);
} else {
read_line(reader, tx, wrapper);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relative_command_path_resolves() {
let cwd_parent = platform::current_exe()
.unwrap()
.parent()
.unwrap()
.parent() .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]
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]
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"
);
}
}