Skip to content

Commit

Permalink
fix: read Command output ending with a carriage return, closes #3508 (#…
Browse files Browse the repository at this point in the history
…3523)

Co-authored-by: chip <chip@chip.sh>
  • Loading branch information
lucasfernog and chippers committed Feb 24, 2022
1 parent 2b554c3 commit 0a0de8a
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changes/command-output-carriage-return.md
@@ -0,0 +1,5 @@
---
"tauri": patch
---

The `tauri::api::process::Command` API now properly reads stdout and stderr messages that ends with a carriage return (`\r`) instead of just a newline (`\n`).
3 changes: 2 additions & 1 deletion core/tauri/Cargo.toml
Expand Up @@ -80,6 +80,7 @@ attohttpc = { version = "0.18", features = [ "json", "form" ], optional = true }
open = { version = "2.0", optional = true }
shared_child = { version = "1.0", optional = true }
os_pipe = { version = "1.0", optional = true }
memchr = { version = "2.4", optional = true }
rfd = { version = "0.7.0", features = [ "parent" ], optional = true }
raw-window-handle = "0.4.2"
minisign-verify = { version = "0.2", optional = true }
Expand Down Expand Up @@ -125,7 +126,7 @@ updater = [ "minisign-verify", "base64", "http-api", "dialog-ask" ]
http-api = [ "attohttpc" ]
shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ]
reqwest-client = [ "reqwest", "bytes" ]
command = [ "shared_child", "os_pipe" ]
command = [ "shared_child", "os_pipe", "memchr" ]
dialog = [ "rfd" ]
notification = [ "notify-rust" ]
cli = [ "clap" ]
Expand Down
135 changes: 99 additions & 36 deletions core/tauri/src/api/process/command.rs
Expand Up @@ -19,8 +19,8 @@ use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;

use crate::async_runtime::{block_on as block_on_task, channel, Receiver};
use os_pipe::{pipe, PipeWriter};
use crate::async_runtime::{block_on as block_on_task, channel, Receiver, Sender};
use os_pipe::{pipe, PipeReader, PipeWriter};
use serde::Serialize;
use shared_child::SharedChild;
use tauri_utils::platform;
Expand Down Expand Up @@ -55,11 +55,11 @@ pub struct TerminatedPayload {
#[serde(tag = "event", content = "payload")]
#[non_exhaustive]
pub enum CommandEvent {
/// Stderr line.
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
Stderr(String),
/// Stdout line.
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
Stdout(String),
/// An error happened.
/// 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),
Expand Down Expand Up @@ -257,37 +257,18 @@ impl Command {

let (tx, rx) = channel(1);

let tx_ = tx.clone();
let guard_ = guard.clone();
spawn(move || {
let _lock = guard_.read().unwrap();
let reader = BufReader::new(stdout_reader);
for line in reader.lines() {
let tx_ = tx_.clone();
block_on_task(async move {
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(move || {
let _lock = guard_.read().unwrap();
let reader = BufReader::new(stderr_reader);
for line in reader.lines() {
let tx_ = tx_.clone();
block_on_task(async move {
let _ = match line {
Ok(line) => tx_.send(CommandEvent::Stderr(line)).await,
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
};
});
}
});
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() {
Expand Down Expand Up @@ -390,6 +371,88 @@ impl Command {
}
}

fn spawn_pipe_reader<F: Fn(String) -> 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);

let mut buf = Vec::new();
loop {
buf.clear();
match read_command_output(&mut reader, &mut buf) {
Ok(n) => {
if n == 0 {
break;
}
let tx_ = tx.clone();
let line = String::from_utf8(buf.clone());
block_on_task(async move {
let _ = match line {
Ok(line) => tx_.send(wrapper(line)).await,
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
};
});
}
Err(e) => {
let tx_ = tx.clone();
let _ = block_on_task(async move { tx_.send(CommandEvent::Error(e.to_string())).await });
}
}
}
});
}

// adapted from https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line
fn read_command_output<R: BufRead + ?Sized>(
r: &mut R,
buf: &mut Vec<u8>,
) -> std::io::Result<usize> {
let mut read = 0;
loop {
let (done, used) = {
let available = match r.fill_buf() {
Ok(n) => n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
match memchr::memchr(b'\n', available) {
Some(i) => {
let end = i + 1;
buf.extend_from_slice(&available[..end]);
(true, end)
}
None => match memchr::memchr(b'\r', available) {
Some(i) => {
let end = i + 1;
buf.extend_from_slice(&available[..end]);
(true, end)
}
None => {
buf.extend_from_slice(available);
(false, available.len())
}
},
}
};
r.consume(used);
read += used;
if done || used == 0 {
if buf.ends_with(&[b'\n']) {
buf.pop();
}
if buf.ends_with(&[b'\r']) {
buf.pop();
}
return Ok(read);
}
}
}

// tests for the commands functions.
#[cfg(test)]
mod test {
Expand Down

0 comments on commit 0a0de8a

Please sign in to comment.