feat(shell) raw-encoded pipe reader directly outputs buffer (no newline scan) (#1231)

* Shell raw-encoded pipe reader directly outputs buffer (no newline scan)

* Suggestions from code review and add .changes file

* fmt
This commit is contained in:
Graham Held
2024-05-02 06:00:03 -07:00
committed by GitHub
parent aea748ced7
commit b4efa58d5d
3 changed files with 99 additions and 34 deletions
+89 -33
View File
@@ -4,7 +4,7 @@
use std::{
ffi::OsStr,
io::{BufReader, Write},
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Command as StdCommand, Stdio},
sync::{Arc, RwLock},
@@ -41,11 +41,13 @@ pub struct TerminatedPayload {
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CommandEvent {
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
/// If configured for raw output, all bytes written to stderr.
/// Otherwise, 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.
/// If configured for raw output, all bytes written to stdout.
/// Otherwise, 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.
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to a UTF-8 string.
Error(String),
/// Command process terminated.
Terminated(TerminatedPayload),
@@ -53,7 +55,10 @@ pub enum CommandEvent {
/// The type to spawn commands.
#[derive(Debug)]
pub struct Command(StdCommand);
pub struct Command {
cmd: StdCommand,
raw_out: bool,
}
/// Spawned child process.
#[derive(Debug)]
@@ -122,7 +127,7 @@ fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
impl From<Command> for StdCommand {
fn from(cmd: Command) -> StdCommand {
cmd.0
cmd.cmd
}
}
@@ -136,7 +141,10 @@ impl Command {
#[cfg(windows)]
command.creation_flags(CREATE_NO_WINDOW);
Self(command)
Self {
cmd: command,
raw_out: false,
}
}
pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
@@ -146,7 +154,7 @@ impl Command {
/// Appends an argument to the command.
#[must_use]
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.0.arg(arg);
self.cmd.arg(arg);
self
}
@@ -157,14 +165,14 @@ impl Command {
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.0.args(args);
self.cmd.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.cmd.env_clear();
self
}
@@ -175,7 +183,7 @@ impl Command {
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.env(key, value);
self.cmd.env(key, value);
self
}
@@ -187,14 +195,20 @@ impl Command {
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(envs);
self.cmd.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.cmd.current_dir(current_dir);
self
}
/// Configures the reader to output bytes from the child process exactly as received
pub fn set_raw_out(mut self, raw_out: bool) -> Self {
self.raw_out = raw_out;
self
}
@@ -229,6 +243,7 @@ impl Command {
/// });
/// ```
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()?;
@@ -249,12 +264,14 @@ impl Command {
guard.clone(),
stdout_reader,
CommandEvent::Stdout,
raw,
);
spawn_pipe_reader(
tx.clone(),
guard.clone(),
stderr_reader,
CommandEvent::Stderr,
raw,
);
spawn(move || {
@@ -359,35 +376,74 @@ impl Command {
}
}
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 mut reader = BufReader::new(pipe_reader);
let 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 },
);
break;
}
}
if raw_out {
read_raw_bytes(reader, tx, wrapper);
} else {
read_line(reader, tx, wrapper);
}
});
}