Skip to content

Commit 509d467

Browse files
cymruulucasfernog
andauthored
Support sending raw byte data to the "data" event for child command's stdout and stderr (#5789)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio> Co-authored-by: Lucas Nogueira <lucas@tauri.app>
1 parent adf4627 commit 509d467

File tree

10 files changed

+364
-138
lines changed

10 files changed

+364
-138
lines changed

.changes/raw-encoding.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"api": minor
3+
"tauri": minor
4+
---
5+
6+
Added `raw` encoding option to read stdout and stderr raw bytes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"api": minor
3+
---
4+
5+
Removed shell's `Command` constructor and added the `Command.create` static function instead.

core/tauri-build/src/mobile.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl PluginBuilder {
7878
let tauri_library_path = std::env::var("DEP_TAURI_IOS_LIBRARY_PATH")
7979
.expect("missing `DEP_TAURI_IOS_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin.");
8080

81-
let tauri_dep_path = &path.parent().unwrap().join(".tauri");
81+
let tauri_dep_path = path.parent().unwrap().join(".tauri");
8282
create_dir_all(&tauri_dep_path).context("failed to create .tauri directory")?;
8383
copy_folder(
8484
Path::new(&tauri_library_path),

core/tauri/scripts/bundle.global.js

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ use std::os::windows::process::CommandExt;
1818

1919
#[cfg(windows)]
2020
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
21+
const NEWLINE_BYTE: u8 = b'\n';
2122

2223
use crate::async_runtime::{block_on as block_on_task, channel, Receiver, Sender};
24+
2325
pub use encoding_rs::Encoding;
2426
use os_pipe::{pipe, PipeReader, PipeWriter};
2527
use serde::Serialize;
@@ -54,14 +56,13 @@ pub struct TerminatedPayload {
5456
}
5557

5658
/// A event sent to the command callback.
57-
#[derive(Debug, Clone, Serialize)]
58-
#[serde(tag = "event", content = "payload")]
59+
#[derive(Debug, Clone)]
5960
#[non_exhaustive]
6061
pub enum CommandEvent {
6162
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
62-
Stderr(String),
63+
Stderr(Vec<u8>),
6364
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
64-
Stdout(String),
65+
Stdout(Vec<u8>),
6566
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string.
6667
Error(String),
6768
/// Command process terminated.
@@ -76,7 +77,6 @@ pub struct Command {
7677
env_clear: bool,
7778
env: HashMap<String, String>,
7879
current_dir: Option<PathBuf>,
79-
encoding: Option<&'static Encoding>,
8080
}
8181

8282
/// Spawned child process.
@@ -129,9 +129,9 @@ pub struct Output {
129129
/// The status (exit code) of the process.
130130
pub status: ExitStatus,
131131
/// The data that the process wrote to stdout.
132-
pub stdout: String,
132+
pub stdout: Vec<u8>,
133133
/// The data that the process wrote to stderr.
134-
pub stderr: String,
134+
pub stderr: Vec<u8>,
135135
}
136136

137137
fn relative_command_path(command: String) -> crate::Result<String> {
@@ -173,7 +173,6 @@ impl Command {
173173
env_clear: false,
174174
env: Default::default(),
175175
current_dir: None,
176-
encoding: None,
177176
}
178177
}
179178

@@ -219,13 +218,6 @@ impl Command {
219218
self
220219
}
221220

222-
/// Sets the character encoding for stdout/stderr.
223-
#[must_use]
224-
pub fn encoding(mut self, encoding: &'static Encoding) -> Self {
225-
self.encoding.replace(encoding);
226-
self
227-
}
228-
229221
/// Spawns the command.
230222
///
231223
/// # Examples
@@ -241,7 +233,7 @@ impl Command {
241233
/// let mut i = 0;
242234
/// while let Some(event) = rx.recv().await {
243235
/// if let CommandEvent::Stdout(line) = event {
244-
/// println!("got: {}", line);
236+
/// println!("got: {}", String::from_utf8(line).unwrap());
245237
/// i += 1;
246238
/// if i == 4 {
247239
/// child.write("message from Rust\n".as_bytes()).unwrap();
@@ -252,7 +244,6 @@ impl Command {
252244
/// });
253245
/// ```
254246
pub fn spawn(self) -> crate::api::Result<(Receiver<CommandEvent>, CommandChild)> {
255-
let encoding = self.encoding;
256247
let mut command: StdCommand = self.into();
257248
let (stdout_reader, stdout_writer) = pipe()?;
258249
let (stderr_reader, stderr_writer) = pipe()?;
@@ -275,14 +266,12 @@ impl Command {
275266
guard.clone(),
276267
stdout_reader,
277268
CommandEvent::Stdout,
278-
encoding,
279269
);
280270
spawn_pipe_reader(
281271
tx.clone(),
282272
guard.clone(),
283273
stderr_reader,
284274
CommandEvent::Stderr,
285-
encoding,
286275
);
287276

288277
spawn(move || {
@@ -350,27 +339,28 @@ impl Command {
350339
/// use tauri::api::process::Command;
351340
/// let output = Command::new("echo").args(["TAURI"]).output().unwrap();
352341
/// assert!(output.status.success());
353-
/// assert_eq!(output.stdout, "TAURI");
342+
/// assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
354343
/// ```
355344
pub fn output(self) -> crate::api::Result<Output> {
356345
let (mut rx, _child) = self.spawn()?;
357346

358347
let output = crate::async_runtime::safe_block_on(async move {
359348
let mut code = None;
360-
let mut stdout = String::new();
361-
let mut stderr = String::new();
349+
let mut stdout = Vec::new();
350+
let mut stderr = Vec::new();
351+
362352
while let Some(event) = rx.recv().await {
363353
match event {
364354
CommandEvent::Terminated(payload) => {
365355
code = payload.code;
366356
}
367357
CommandEvent::Stdout(line) => {
368-
stdout.push_str(line.as_str());
369-
stdout.push('\n');
358+
stdout.extend(line);
359+
stdout.push(NEWLINE_BYTE);
370360
}
371361
CommandEvent::Stderr(line) => {
372-
stderr.push_str(line.as_str());
373-
stderr.push('\n');
362+
stderr.extend(line);
363+
stderr.push(NEWLINE_BYTE);
374364
}
375365
CommandEvent::Error(_) => {}
376366
}
@@ -386,36 +376,25 @@ impl Command {
386376
}
387377
}
388378

389-
fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
379+
fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
390380
tx: Sender<CommandEvent>,
391381
guard: Arc<RwLock<()>>,
392382
pipe_reader: PipeReader,
393383
wrapper: F,
394-
character_encoding: Option<&'static Encoding>,
395384
) {
396385
spawn(move || {
397386
let _lock = guard.read().unwrap();
398387
let mut reader = BufReader::new(pipe_reader);
399388

400-
let mut buf = Vec::new();
401389
loop {
402-
buf.clear();
390+
let mut buf = Vec::new();
403391
match tauri_utils::io::read_line(&mut reader, &mut buf) {
404392
Ok(n) => {
405393
if n == 0 {
406394
break;
407395
}
408396
let tx_ = tx.clone();
409-
let line = match character_encoding {
410-
Some(encoding) => Ok(encoding.decode_with_bom_removal(&buf).0.into()),
411-
None => String::from_utf8(buf.clone()),
412-
};
413-
block_on_task(async move {
414-
let _ = match line {
415-
Ok(line) => tx_.send(wrapper(line)).await,
416-
Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
417-
};
418-
});
397+
let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
419398
}
420399
Err(e) => {
421400
let tx_ = tx.clone();
@@ -428,14 +407,34 @@ fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
428407

429408
// tests for the commands functions.
430409
#[cfg(test)]
431-
mod test {
410+
mod tests {
432411
#[cfg(not(windows))]
433412
use super::*;
434413

435414
#[cfg(not(windows))]
436415
#[test]
437-
fn test_cmd_output() {
438-
// create a command to run cat.
416+
fn test_cmd_spawn_output() {
417+
let cmd = Command::new("cat").args(["test/api/test.txt"]);
418+
let (mut rx, _) = cmd.spawn().unwrap();
419+
420+
crate::async_runtime::block_on(async move {
421+
while let Some(event) = rx.recv().await {
422+
match event {
423+
CommandEvent::Terminated(payload) => {
424+
assert_eq!(payload.code, Some(0));
425+
}
426+
CommandEvent::Stdout(line) => {
427+
assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
428+
}
429+
_ => {}
430+
}
431+
}
432+
});
433+
}
434+
435+
#[cfg(not(windows))]
436+
#[test]
437+
fn test_cmd_spawn_raw_output() {
439438
let cmd = Command::new("cat").args(["test/api/test.txt"]);
440439
let (mut rx, _) = cmd.spawn().unwrap();
441440

@@ -446,7 +445,7 @@ mod test {
446445
assert_eq!(payload.code, Some(0));
447446
}
448447
CommandEvent::Stdout(line) => {
449-
assert_eq!(line, "This is a test doc!".to_string());
448+
assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
450449
}
451450
_ => {}
452451
}
@@ -457,7 +456,32 @@ mod test {
457456
#[cfg(not(windows))]
458457
#[test]
459458
// test the failure case
460-
fn test_cmd_fail() {
459+
fn test_cmd_spawn_fail() {
460+
let cmd = Command::new("cat").args(["test/api/"]);
461+
let (mut rx, _) = cmd.spawn().unwrap();
462+
463+
crate::async_runtime::block_on(async move {
464+
while let Some(event) = rx.recv().await {
465+
match event {
466+
CommandEvent::Terminated(payload) => {
467+
assert_eq!(payload.code, Some(1));
468+
}
469+
CommandEvent::Stderr(line) => {
470+
assert_eq!(
471+
String::from_utf8(line).unwrap(),
472+
"cat: test/api/: Is a directory"
473+
);
474+
}
475+
_ => {}
476+
}
477+
}
478+
});
479+
}
480+
481+
#[cfg(not(windows))]
482+
#[test]
483+
// test the failure case (raw encoding)
484+
fn test_cmd_spawn_raw_fail() {
461485
let cmd = Command::new("cat").args(["test/api/"]);
462486
let (mut rx, _) = cmd.spawn().unwrap();
463487

@@ -468,11 +492,40 @@ mod test {
468492
assert_eq!(payload.code, Some(1));
469493
}
470494
CommandEvent::Stderr(line) => {
471-
assert_eq!(line, "cat: test/api/: Is a directory".to_string());
495+
assert_eq!(
496+
String::from_utf8(line).unwrap(),
497+
"cat: test/api/: Is a directory"
498+
);
472499
}
473500
_ => {}
474501
}
475502
}
476503
});
477504
}
505+
506+
#[cfg(not(windows))]
507+
#[test]
508+
fn test_cmd_output_output() {
509+
let cmd = Command::new("cat").args(["test/api/test.txt"]);
510+
let output = cmd.output().unwrap();
511+
512+
assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
513+
assert_eq!(
514+
String::from_utf8(output.stdout).unwrap(),
515+
"This is a test doc!\n"
516+
);
517+
}
518+
519+
#[cfg(not(windows))]
520+
#[test]
521+
fn test_cmd_output_output_fail() {
522+
let cmd = Command::new("cat").args(["test/api/"]);
523+
let output = cmd.output().unwrap();
524+
525+
assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
526+
assert_eq!(
527+
String::from_utf8(output.stderr).unwrap(),
528+
"cat: test/api/: Is a directory\n"
529+
);
530+
}
478531
}

0 commit comments

Comments
 (0)