tauri_plugin_shell/process/
mod.rs1use std::{
6 ffi::OsStr,
7 io::{BufRead, BufReader, Write},
8 path::{Path, PathBuf},
9 process::{Command as StdCommand, Stdio},
10 sync::Arc,
11 thread::{JoinHandle, spawn},
12};
13
14#[cfg(unix)]
15use std::os::unix::process::ExitStatusExt;
16#[cfg(windows)]
17use std::os::windows::process::CommandExt;
18
19#[cfg(windows)]
20const CREATE_NO_WINDOW: u32 = 0x0800_0000;
21const NEWLINE_BYTE: u8 = b'\n';
22
23use tauri::async_runtime::{Receiver, Sender, block_on as block_on_task, channel};
24
25pub use encoding_rs::Encoding;
26use os_pipe::{PipeReader, PipeWriter, pipe};
27use serde::Serialize;
28use shared_child::SharedChild;
29use tauri::utils::platform;
30
31#[derive(Debug, Clone, Serialize)]
33pub struct TerminatedPayload {
34 pub code: Option<i32>,
36 pub signal: Option<i32>,
38}
39
40#[derive(Debug, Clone)]
42#[non_exhaustive]
43pub enum CommandEvent {
44 Stderr(Vec<u8>),
47 Stdout(Vec<u8>),
50 Error(String),
52 Terminated(TerminatedPayload),
54}
55
56#[derive(Debug)]
58pub struct Command {
59 cmd: StdCommand,
60 raw_out: bool,
61}
62
63#[derive(Debug)]
65pub struct CommandChild {
66 inner: Arc<SharedChild>,
67 stdin_writer: PipeWriter,
68}
69
70impl CommandChild {
71 pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
73 self.stdin_writer.write_all(buf)?;
74 Ok(())
75 }
76
77 pub fn kill(self) -> crate::Result<()> {
79 self.inner.kill()?;
80 Ok(())
81 }
82
83 pub fn pid(&self) -> u32 {
85 self.inner.id()
86 }
87}
88
89#[derive(Debug)]
91pub struct ExitStatus {
92 code: Option<i32>,
95}
96
97impl ExitStatus {
98 pub fn code(&self) -> Option<i32> {
100 self.code
101 }
102
103 pub fn success(&self) -> bool {
105 self.code == Some(0)
106 }
107}
108
109#[derive(Debug)]
111pub struct Output {
112 pub status: ExitStatus,
114 pub stdout: Vec<u8>,
116 pub stderr: Vec<u8>,
118}
119
120fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
121 let exe_path = platform::current_exe()?;
122
123 let exe_dir = exe_path
124 .parent()
125 .ok_or(crate::Error::CurrentExeHasNoParent)?;
126
127 let base_dir = if exe_dir.ends_with("deps") {
129 exe_dir.parent().unwrap_or(exe_dir)
130 } else {
131 exe_dir
132 };
133
134 let mut command_path = base_dir.join(command);
135
136 #[cfg(windows)]
137 {
138 let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
139 if !already_exe {
140 command_path.as_mut_os_string().push(".exe");
142 }
143 }
144
145 #[cfg(not(windows))]
146 {
147 if command_path.extension().is_some_and(|ext| ext == "exe") {
148 command_path.set_extension("");
149 }
150 }
151
152 Ok(command_path)
153}
154
155impl From<Command> for StdCommand {
156 fn from(cmd: Command) -> StdCommand {
157 cmd.cmd
158 }
159}
160
161impl Command {
162 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
163 log::debug!(
164 "Creating sidecar {}",
165 program.as_ref().to_str().unwrap_or("")
166 );
167 let mut command = StdCommand::new(program);
168
169 command.stdout(Stdio::piped());
170 command.stdin(Stdio::piped());
171 command.stderr(Stdio::piped());
172 #[cfg(windows)]
173 command.creation_flags(CREATE_NO_WINDOW);
174
175 Self {
176 cmd: command,
177 raw_out: false,
178 }
179 }
180
181 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
182 Ok(Self::new(relative_command_path(program.as_ref())?))
183 }
184
185 #[must_use]
187 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
188 self.cmd.arg(arg);
189 self
190 }
191
192 #[must_use]
194 pub fn args<I, S>(mut self, args: I) -> Self
195 where
196 I: IntoIterator<Item = S>,
197 S: AsRef<OsStr>,
198 {
199 self.cmd.args(args);
200 self
201 }
202
203 #[must_use]
205 pub fn env_clear(mut self) -> Self {
206 self.cmd.env_clear();
207 self
208 }
209
210 #[must_use]
212 pub fn env<K, V>(mut self, key: K, value: V) -> Self
213 where
214 K: AsRef<OsStr>,
215 V: AsRef<OsStr>,
216 {
217 self.cmd.env(key, value);
218 self
219 }
220
221 #[must_use]
223 pub fn envs<I, K, V>(mut self, envs: I) -> Self
224 where
225 I: IntoIterator<Item = (K, V)>,
226 K: AsRef<OsStr>,
227 V: AsRef<OsStr>,
228 {
229 self.cmd.envs(envs);
230 self
231 }
232
233 #[must_use]
235 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
236 self.cmd.current_dir(current_dir);
237 self
238 }
239
240 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
242 self.raw_out = raw_out;
243 self
244 }
245
246 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
306 let raw = self.raw_out;
307 let mut command: StdCommand = self.into();
308 let (stdout_reader, stdout_writer) = pipe()?;
309 let (stderr_reader, stderr_writer) = pipe()?;
310 let (stdin_reader, stdin_writer) = pipe()?;
311 command.stdout(stdout_writer);
312 command.stderr(stderr_writer);
313 command.stdin(stdin_reader);
314
315 let shared_child = SharedChild::spawn(&mut command)?;
316 let child = Arc::new(shared_child);
317 let child_ = child.clone();
318 let (tx, rx) = channel(1);
319
320 let stdout_thread = spawn_pipe_reader(tx.clone(), stdout_reader, CommandEvent::Stdout, raw);
321 let stderr_thread = spawn_pipe_reader(tx.clone(), stderr_reader, CommandEvent::Stderr, raw);
322
323 spawn(move || {
324 let status = child_.wait();
325 let _ = stdout_thread.join();
328 let _ = stderr_thread.join();
329 let _ = match status {
330 Ok(status) => block_on_task(async move {
331 tx.send(CommandEvent::Terminated(TerminatedPayload {
332 code: status.code(),
333 #[cfg(windows)]
334 signal: None,
335 #[cfg(unix)]
336 signal: status.signal(),
337 }))
338 .await
339 }),
340 Err(e) => {
341 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
342 }
343 };
344 });
345
346 Ok((
347 rx,
348 CommandChild {
349 inner: child,
350 stdin_writer,
351 },
352 ))
353 }
354
355 pub async fn status(self) -> crate::Result<ExitStatus> {
369 let (mut rx, _child) = self.spawn()?;
370 let mut code = None;
371 #[allow(clippy::collapsible_match)]
372 while let Some(event) = rx.recv().await {
373 if let CommandEvent::Terminated(payload) = event {
374 code = payload.code;
375 }
376 }
377 Ok(ExitStatus { code })
378 }
379
380 pub async fn output(self) -> crate::Result<Output> {
396 let (mut rx, _child) = self.spawn()?;
397
398 let mut code = None;
399 let mut stdout = Vec::new();
400 let mut stderr = Vec::new();
401
402 while let Some(event) = rx.recv().await {
403 match event {
404 CommandEvent::Terminated(payload) => {
405 code = payload.code;
406 }
407 CommandEvent::Stdout(line) => {
408 stdout.extend(line);
409 stdout.push(NEWLINE_BYTE);
410 }
411 CommandEvent::Stderr(line) => {
412 stderr.extend(line);
413 stderr.push(NEWLINE_BYTE);
414 }
415 CommandEvent::Error(_) => {}
416 }
417 }
418 Ok(Output {
419 status: ExitStatus { code },
420 stdout,
421 stderr,
422 })
423 }
424}
425
426fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
427 mut reader: BufReader<PipeReader>,
428 tx: Sender<CommandEvent>,
429 wrapper: F,
430) {
431 loop {
432 let result = reader.fill_buf();
433 match result {
434 Ok(buf) => {
435 let length = buf.len();
436 if length == 0 {
437 break;
438 }
439 let tx_ = tx.clone();
440 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
441 reader.consume(length);
442 }
443 Err(e) => {
444 let tx_ = tx.clone();
445 let _ = block_on_task(
446 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
447 );
448 }
449 }
450 }
451}
452
453fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
454 mut reader: BufReader<PipeReader>,
455 tx: Sender<CommandEvent>,
456 wrapper: F,
457) {
458 loop {
459 let mut buf = Vec::new();
460 match tauri::utils::io::read_line(&mut reader, &mut buf) {
461 Ok(n) => {
462 if n == 0 {
463 break;
464 }
465 let tx_ = tx.clone();
466 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
467 }
468 Err(e) => {
469 let _ =
470 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await });
471 break;
472 }
473 }
474 }
475}
476
477fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
478 tx: Sender<CommandEvent>,
479 pipe_reader: PipeReader,
480 wrapper: F,
481 raw_out: bool,
482) -> JoinHandle<()> {
483 spawn(move || {
484 let reader = BufReader::new(pipe_reader);
485
486 if raw_out {
487 read_raw_bytes(reader, tx, wrapper);
488 } else {
489 read_line(reader, tx, wrapper);
490 }
491 })
492}
493
494#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn relative_command_path_resolves() {
501 let cwd_parent = platform::current_exe()
502 .unwrap()
503 .parent()
504 .unwrap()
505 .parent() .unwrap()
507 .to_owned();
508 assert_eq!(
509 relative_command_path(Path::new("Tauri.Example")).unwrap(),
510 cwd_parent.join(if cfg!(windows) {
511 "Tauri.Example.exe"
512 } else {
513 "Tauri.Example"
514 })
515 );
516 assert_eq!(
517 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
518 cwd_parent.join(if cfg!(windows) {
519 "Tauri.Example.exe"
520 } else {
521 "Tauri.Example"
522 })
523 );
524 }
525
526 #[cfg(not(windows))]
527 #[test]
528 fn test_cmd_spawn_output() {
529 let cmd = Command::new("cat").args(["test/test.txt"]);
530 let (mut rx, _) = cmd.spawn().unwrap();
531
532 tauri::async_runtime::block_on(async move {
533 while let Some(event) = rx.recv().await {
534 match event {
535 CommandEvent::Terminated(payload) => {
536 assert_eq!(payload.code, Some(0));
537 }
538 CommandEvent::Stdout(line) => {
539 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
540 }
541 _ => {}
542 }
543 }
544 });
545 }
546
547 #[cfg(not(windows))]
548 #[test]
549 fn test_cmd_spawn_raw_output() {
550 let cmd = Command::new("cat").args(["test/test.txt"]);
551 let (mut rx, _) = cmd.spawn().unwrap();
552
553 tauri::async_runtime::block_on(async move {
554 while let Some(event) = rx.recv().await {
555 match event {
556 CommandEvent::Terminated(payload) => {
557 assert_eq!(payload.code, Some(0));
558 }
559 CommandEvent::Stdout(line) => {
560 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
561 }
562 _ => {}
563 }
564 }
565 });
566 }
567
568 #[cfg(not(windows))]
569 #[test]
570 fn test_cmd_spawn_fail() {
572 let cmd = Command::new("cat").args(["test/"]);
573 let (mut rx, _) = cmd.spawn().unwrap();
574
575 tauri::async_runtime::block_on(async move {
576 while let Some(event) = rx.recv().await {
577 match event {
578 CommandEvent::Terminated(payload) => {
579 assert_eq!(payload.code, Some(1));
580 }
581 CommandEvent::Stderr(line) => {
582 assert_eq!(
583 String::from_utf8(line).unwrap(),
584 "cat: test/: Is a directory\n"
585 );
586 }
587 _ => {}
588 }
589 }
590 });
591 }
592
593 #[cfg(not(windows))]
594 #[test]
595 fn test_cmd_spawn_raw_fail() {
597 let cmd = Command::new("cat").args(["test/"]);
598 let (mut rx, _) = cmd.spawn().unwrap();
599
600 tauri::async_runtime::block_on(async move {
601 while let Some(event) = rx.recv().await {
602 match event {
603 CommandEvent::Terminated(payload) => {
604 assert_eq!(payload.code, Some(1));
605 }
606 CommandEvent::Stderr(line) => {
607 assert_eq!(
608 String::from_utf8(line).unwrap(),
609 "cat: test/: Is a directory\n"
610 );
611 }
612 _ => {}
613 }
614 }
615 });
616 }
617
618 #[cfg(not(windows))]
619 #[test]
620 fn test_cmd_output_output() {
621 let cmd = Command::new("cat").args(["test/test.txt"]);
622 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
623
624 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
625 assert_eq!(
626 String::from_utf8(output.stdout).unwrap(),
627 "This is a test doc!\n"
628 );
629 }
630
631 #[cfg(not(windows))]
632 #[test]
633 fn test_cmd_output_output_fail() {
634 let cmd = Command::new("cat").args(["test/"]);
635 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
636
637 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
638 assert_eq!(
639 String::from_utf8(output.stderr).unwrap(),
640 "cat: test/: Is a directory\n\n"
641 );
642 }
643}