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, RwLock},
11 thread::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::{block_on as block_on_task, channel, Receiver, Sender};
24
25pub use encoding_rs::Encoding;
26use os_pipe::{pipe, PipeReader, PipeWriter};
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 match platform::current_exe()?.parent() {
122 #[cfg(windows)]
123 Some(exe_dir) => {
124 let mut command_path = exe_dir.join(command);
125 let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
126 if !already_exe {
127 command_path.as_mut_os_string().push(".exe");
129 }
130 Ok(command_path)
131 }
132 #[cfg(not(windows))]
133 Some(exe_dir) => {
134 let mut command_path = exe_dir.join(command);
135 if command_path.extension().is_some_and(|ext| ext == "exe") {
136 command_path.set_extension("");
137 }
138 Ok(command_path)
139 }
140 None => Err(crate::Error::CurrentExeHasNoParent),
141 }
142}
143
144impl From<Command> for StdCommand {
145 fn from(cmd: Command) -> StdCommand {
146 cmd.cmd
147 }
148}
149
150impl Command {
151 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
152 log::debug!(
153 "Creating sidecar {}",
154 program.as_ref().to_str().unwrap_or("")
155 );
156 let mut command = StdCommand::new(program);
157
158 command.stdout(Stdio::piped());
159 command.stdin(Stdio::piped());
160 command.stderr(Stdio::piped());
161 #[cfg(windows)]
162 command.creation_flags(CREATE_NO_WINDOW);
163
164 Self {
165 cmd: command,
166 raw_out: false,
167 }
168 }
169
170 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
171 Ok(Self::new(relative_command_path(program.as_ref())?))
172 }
173
174 #[must_use]
176 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
177 self.cmd.arg(arg);
178 self
179 }
180
181 #[must_use]
183 pub fn args<I, S>(mut self, args: I) -> Self
184 where
185 I: IntoIterator<Item = S>,
186 S: AsRef<OsStr>,
187 {
188 self.cmd.args(args);
189 self
190 }
191
192 #[must_use]
194 pub fn env_clear(mut self) -> Self {
195 self.cmd.env_clear();
196 self
197 }
198
199 #[must_use]
201 pub fn env<K, V>(mut self, key: K, value: V) -> Self
202 where
203 K: AsRef<OsStr>,
204 V: AsRef<OsStr>,
205 {
206 self.cmd.env(key, value);
207 self
208 }
209
210 #[must_use]
212 pub fn envs<I, K, V>(mut self, envs: I) -> Self
213 where
214 I: IntoIterator<Item = (K, V)>,
215 K: AsRef<OsStr>,
216 V: AsRef<OsStr>,
217 {
218 self.cmd.envs(envs);
219 self
220 }
221
222 #[must_use]
224 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
225 self.cmd.current_dir(current_dir);
226 self
227 }
228
229 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
231 self.raw_out = raw_out;
232 self
233 }
234
235 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
295 let raw = self.raw_out;
296 let mut command: StdCommand = self.into();
297 let (stdout_reader, stdout_writer) = pipe()?;
298 let (stderr_reader, stderr_writer) = pipe()?;
299 let (stdin_reader, stdin_writer) = pipe()?;
300 command.stdout(stdout_writer);
301 command.stderr(stderr_writer);
302 command.stdin(stdin_reader);
303
304 let shared_child = SharedChild::spawn(&mut command)?;
305 let child = Arc::new(shared_child);
306 let child_ = child.clone();
307 let guard = Arc::new(RwLock::new(()));
308
309 let (tx, rx) = channel(1);
310
311 spawn_pipe_reader(
312 tx.clone(),
313 guard.clone(),
314 stdout_reader,
315 CommandEvent::Stdout,
316 raw,
317 );
318 spawn_pipe_reader(
319 tx.clone(),
320 guard.clone(),
321 stderr_reader,
322 CommandEvent::Stderr,
323 raw,
324 );
325
326 spawn(move || {
327 let _ = match child_.wait() {
328 Ok(status) => {
329 let _l = guard.write().unwrap();
330 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 }
341 Err(e) => {
342 let _l = guard.write().unwrap();
343 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
344 }
345 };
346 });
347
348 Ok((
349 rx,
350 CommandChild {
351 inner: child,
352 stdin_writer,
353 },
354 ))
355 }
356
357 pub async fn status(self) -> crate::Result<ExitStatus> {
371 let (mut rx, _child) = self.spawn()?;
372 let mut code = None;
373 #[allow(clippy::collapsible_match)]
374 while let Some(event) = rx.recv().await {
375 if let CommandEvent::Terminated(payload) = event {
376 code = payload.code;
377 }
378 }
379 Ok(ExitStatus { code })
380 }
381
382 pub async fn output(self) -> crate::Result<Output> {
398 let (mut rx, _child) = self.spawn()?;
399
400 let mut code = None;
401 let mut stdout = Vec::new();
402 let mut stderr = Vec::new();
403
404 while let Some(event) = rx.recv().await {
405 match event {
406 CommandEvent::Terminated(payload) => {
407 code = payload.code;
408 }
409 CommandEvent::Stdout(line) => {
410 stdout.extend(line);
411 stdout.push(NEWLINE_BYTE);
412 }
413 CommandEvent::Stderr(line) => {
414 stderr.extend(line);
415 stderr.push(NEWLINE_BYTE);
416 }
417 CommandEvent::Error(_) => {}
418 }
419 }
420 Ok(Output {
421 status: ExitStatus { code },
422 stdout,
423 stderr,
424 })
425 }
426}
427
428fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
429 mut reader: BufReader<PipeReader>,
430 tx: Sender<CommandEvent>,
431 wrapper: F,
432) {
433 loop {
434 let result = reader.fill_buf();
435 match result {
436 Ok(buf) => {
437 let length = buf.len();
438 if length == 0 {
439 break;
440 }
441 let tx_ = tx.clone();
442 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
443 reader.consume(length);
444 }
445 Err(e) => {
446 let tx_ = tx.clone();
447 let _ = block_on_task(
448 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
449 );
450 }
451 }
452 }
453}
454
455fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
456 mut reader: BufReader<PipeReader>,
457 tx: Sender<CommandEvent>,
458 wrapper: F,
459) {
460 loop {
461 let mut buf = Vec::new();
462 match tauri::utils::io::read_line(&mut reader, &mut buf) {
463 Ok(n) => {
464 if n == 0 {
465 break;
466 }
467 let tx_ = tx.clone();
468 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
469 }
470 Err(e) => {
471 let tx_ = tx.clone();
472 let _ = block_on_task(
473 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
474 );
475 break;
476 }
477 }
478 }
479}
480
481fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
482 tx: Sender<CommandEvent>,
483 guard: Arc<RwLock<()>>,
484 pipe_reader: PipeReader,
485 wrapper: F,
486 raw_out: bool,
487) {
488 spawn(move || {
489 let _lock = guard.read().unwrap();
490 let reader = BufReader::new(pipe_reader);
491
492 if raw_out {
493 read_raw_bytes(reader, tx, wrapper);
494 } else {
495 read_line(reader, tx, wrapper);
496 }
497 });
498}
499
500#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn relative_command_path_resolves() {
507 let cwd_parent = platform::current_exe()
508 .unwrap()
509 .parent()
510 .unwrap()
511 .to_owned();
512 assert_eq!(
513 relative_command_path(Path::new("Tauri.Example")).unwrap(),
514 cwd_parent.join(if cfg!(windows) {
515 "Tauri.Example.exe"
516 } else {
517 "Tauri.Example"
518 })
519 );
520 assert_eq!(
521 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
522 cwd_parent.join(if cfg!(windows) {
523 "Tauri.Example.exe"
524 } else {
525 "Tauri.Example"
526 })
527 );
528 }
529
530 #[cfg(not(windows))]
531 #[test]
532 fn test_cmd_spawn_output() {
533 let cmd = Command::new("cat").args(["test/test.txt"]);
534 let (mut rx, _) = cmd.spawn().unwrap();
535
536 tauri::async_runtime::block_on(async move {
537 while let Some(event) = rx.recv().await {
538 match event {
539 CommandEvent::Terminated(payload) => {
540 assert_eq!(payload.code, Some(0));
541 }
542 CommandEvent::Stdout(line) => {
543 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
544 }
545 _ => {}
546 }
547 }
548 });
549 }
550
551 #[cfg(not(windows))]
552 #[test]
553 fn test_cmd_spawn_raw_output() {
554 let cmd = Command::new("cat").args(["test/test.txt"]);
555 let (mut rx, _) = cmd.spawn().unwrap();
556
557 tauri::async_runtime::block_on(async move {
558 while let Some(event) = rx.recv().await {
559 match event {
560 CommandEvent::Terminated(payload) => {
561 assert_eq!(payload.code, Some(0));
562 }
563 CommandEvent::Stdout(line) => {
564 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
565 }
566 _ => {}
567 }
568 }
569 });
570 }
571
572 #[cfg(not(windows))]
573 #[test]
574 fn test_cmd_spawn_fail() {
576 let cmd = Command::new("cat").args(["test/"]);
577 let (mut rx, _) = cmd.spawn().unwrap();
578
579 tauri::async_runtime::block_on(async move {
580 while let Some(event) = rx.recv().await {
581 match event {
582 CommandEvent::Terminated(payload) => {
583 assert_eq!(payload.code, Some(1));
584 }
585 CommandEvent::Stderr(line) => {
586 assert_eq!(
587 String::from_utf8(line).unwrap(),
588 "cat: test/: Is a directory\n"
589 );
590 }
591 _ => {}
592 }
593 }
594 });
595 }
596
597 #[cfg(not(windows))]
598 #[test]
599 fn test_cmd_spawn_raw_fail() {
601 let cmd = Command::new("cat").args(["test/"]);
602 let (mut rx, _) = cmd.spawn().unwrap();
603
604 tauri::async_runtime::block_on(async move {
605 while let Some(event) = rx.recv().await {
606 match event {
607 CommandEvent::Terminated(payload) => {
608 assert_eq!(payload.code, Some(1));
609 }
610 CommandEvent::Stderr(line) => {
611 assert_eq!(
612 String::from_utf8(line).unwrap(),
613 "cat: test/: Is a directory\n"
614 );
615 }
616 _ => {}
617 }
618 }
619 });
620 }
621
622 #[cfg(not(windows))]
623 #[test]
624 fn test_cmd_output_output() {
625 let cmd = Command::new("cat").args(["test/test.txt"]);
626 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
627
628 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
629 assert_eq!(
630 String::from_utf8(output.stdout).unwrap(),
631 "This is a test doc!\n"
632 );
633 }
634
635 #[cfg(not(windows))]
636 #[test]
637 fn test_cmd_output_output_fail() {
638 let cmd = Command::new("cat").args(["test/"]);
639 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
640
641 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
642 assert_eq!(
643 String::from_utf8(output.stderr).unwrap(),
644 "cat: test/: Is a directory\n\n"
645 );
646 }
647}