From 0a0de8ab6ed80b7722012e83636dff41a813b770 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Thu, 24 Feb 2022 12:10:43 -0300 Subject: [PATCH] fix: read Command output ending with a carriage return, closes #3508 (#3523) Co-authored-by: chip --- .changes/command-output-carriage-return.md | 5 + core/tauri/Cargo.toml | 3 +- core/tauri/src/api/process/command.rs | 135 +++++++++++++++------ 3 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 .changes/command-output-carriage-return.md diff --git a/.changes/command-output-carriage-return.md b/.changes/command-output-carriage-return.md new file mode 100644 index 000000000..54994e08c --- /dev/null +++ b/.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`). diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 89d18b0b5..07dcff0c5 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -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 } @@ -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" ] diff --git a/core/tauri/src/api/process/command.rs b/core/tauri/src/api/process/command.rs index 531624a5a..ca78d0f70 100644 --- a/core/tauri/src/api/process/command.rs +++ b/core/tauri/src/api/process/command.rs @@ -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; @@ -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), @@ -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() { @@ -390,6 +371,88 @@ impl Command { } } +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); + + 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: &mut R, + buf: &mut Vec, +) -> std::io::Result { + 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 {