Skip to content

Commit 0a0de8a

Browse files
fix: read Command output ending with a carriage return, closes #3508 (#3523)
Co-authored-by: chip <chip@chip.sh>
1 parent 2b554c3 commit 0a0de8a

File tree

3 files changed

+106
-37
lines changed

3 files changed

+106
-37
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
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`).

core/tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ attohttpc = { version = "0.18", features = [ "json", "form" ], optional = true }
8080
open = { version = "2.0", optional = true }
8181
shared_child = { version = "1.0", optional = true }
8282
os_pipe = { version = "1.0", optional = true }
83+
memchr = { version = "2.4", optional = true }
8384
rfd = { version = "0.7.0", features = [ "parent" ], optional = true }
8485
raw-window-handle = "0.4.2"
8586
minisign-verify = { version = "0.2", optional = true }
@@ -125,7 +126,7 @@ updater = [ "minisign-verify", "base64", "http-api", "dialog-ask" ]
125126
http-api = [ "attohttpc" ]
126127
shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ]
127128
reqwest-client = [ "reqwest", "bytes" ]
128-
command = [ "shared_child", "os_pipe" ]
129+
command = [ "shared_child", "os_pipe", "memchr" ]
129130
dialog = [ "rfd" ]
130131
notification = [ "notify-rust" ]
131132
cli = [ "clap" ]

core/tauri/src/api/process/command.rs

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use std::os::windows::process::CommandExt;
1919
#[cfg(windows)]
2020
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
2121

22-
use crate::async_runtime::{block_on as block_on_task, channel, Receiver};
23-
use os_pipe::{pipe, PipeWriter};
22+
use crate::async_runtime::{block_on as block_on_task, channel, Receiver, Sender};
23+
use os_pipe::{pipe, PipeReader, PipeWriter};
2424
use serde::Serialize;
2525
use shared_child::SharedChild;
2626
use tauri_utils::platform;
@@ -55,11 +55,11 @@ pub struct TerminatedPayload {
5555
#[serde(tag = "event", content = "payload")]
5656
#[non_exhaustive]
5757
pub enum CommandEvent {
58-
/// Stderr line.
58+
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
5959
Stderr(String),
60-
/// Stdout line.
60+
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
6161
Stdout(String),
62-
/// An error happened.
62+
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string.
6363
Error(String),
6464
/// Command process terminated.
6565
Terminated(TerminatedPayload),
@@ -257,37 +257,18 @@ impl Command {
257257

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

260-
let tx_ = tx.clone();
261-
let guard_ = guard.clone();
262-
spawn(move || {
263-
let _lock = guard_.read().unwrap();
264-
let reader = BufReader::new(stdout_reader);
265-
for line in reader.lines() {
266-
let tx_ = tx_.clone();
267-
block_on_task(async move {
268-
let _ = match line {
269-
Ok(line) => tx_.send(CommandEvent::Stdout(line)).await,
270-
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
271-
};
272-
});
273-
}
274-
});
275-
276-
let tx_ = tx.clone();
277-
let guard_ = guard.clone();
278-
spawn(move || {
279-
let _lock = guard_.read().unwrap();
280-
let reader = BufReader::new(stderr_reader);
281-
for line in reader.lines() {
282-
let tx_ = tx_.clone();
283-
block_on_task(async move {
284-
let _ = match line {
285-
Ok(line) => tx_.send(CommandEvent::Stderr(line)).await,
286-
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
287-
};
288-
});
289-
}
290-
});
260+
spawn_pipe_reader(
261+
tx.clone(),
262+
guard.clone(),
263+
stdout_reader,
264+
CommandEvent::Stdout,
265+
);
266+
spawn_pipe_reader(
267+
tx.clone(),
268+
guard.clone(),
269+
stderr_reader,
270+
CommandEvent::Stderr,
271+
);
291272

292273
spawn(move || {
293274
let _ = match child_.wait() {
@@ -390,6 +371,88 @@ impl Command {
390371
}
391372
}
392373

374+
fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
375+
tx: Sender<CommandEvent>,
376+
guard: Arc<RwLock<()>>,
377+
pipe_reader: PipeReader,
378+
wrapper: F,
379+
) {
380+
spawn(move || {
381+
let _lock = guard.read().unwrap();
382+
let mut reader = BufReader::new(pipe_reader);
383+
384+
let mut buf = Vec::new();
385+
loop {
386+
buf.clear();
387+
match read_command_output(&mut reader, &mut buf) {
388+
Ok(n) => {
389+
if n == 0 {
390+
break;
391+
}
392+
let tx_ = tx.clone();
393+
let line = String::from_utf8(buf.clone());
394+
block_on_task(async move {
395+
let _ = match line {
396+
Ok(line) => tx_.send(wrapper(line)).await,
397+
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
398+
};
399+
});
400+
}
401+
Err(e) => {
402+
let tx_ = tx.clone();
403+
let _ = block_on_task(async move { tx_.send(CommandEvent::Error(e.to_string())).await });
404+
}
405+
}
406+
}
407+
});
408+
}
409+
410+
// adapted from https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line
411+
fn read_command_output<R: BufRead + ?Sized>(
412+
r: &mut R,
413+
buf: &mut Vec<u8>,
414+
) -> std::io::Result<usize> {
415+
let mut read = 0;
416+
loop {
417+
let (done, used) = {
418+
let available = match r.fill_buf() {
419+
Ok(n) => n,
420+
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
421+
Err(e) => return Err(e),
422+
};
423+
match memchr::memchr(b'\n', available) {
424+
Some(i) => {
425+
let end = i + 1;
426+
buf.extend_from_slice(&available[..end]);
427+
(true, end)
428+
}
429+
None => match memchr::memchr(b'\r', available) {
430+
Some(i) => {
431+
let end = i + 1;
432+
buf.extend_from_slice(&available[..end]);
433+
(true, end)
434+
}
435+
None => {
436+
buf.extend_from_slice(available);
437+
(false, available.len())
438+
}
439+
},
440+
}
441+
};
442+
r.consume(used);
443+
read += used;
444+
if done || used == 0 {
445+
if buf.ends_with(&[b'\n']) {
446+
buf.pop();
447+
}
448+
if buf.ends_with(&[b'\r']) {
449+
buf.pop();
450+
}
451+
return Ok(read);
452+
}
453+
}
454+
}
455+
393456
// tests for the commands functions.
394457
#[cfg(test)]
395458
mod test {

0 commit comments

Comments
 (0)