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 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 guard = Arc::new(RwLock::new(()));
319
320 let (tx, rx) = channel(1);
321
322 spawn_pipe_reader(
323 tx.clone(),
324 guard.clone(),
325 stdout_reader,
326 CommandEvent::Stdout,
327 raw,
328 );
329 spawn_pipe_reader(
330 tx.clone(),
331 guard.clone(),
332 stderr_reader,
333 CommandEvent::Stderr,
334 raw,
335 );
336
337 spawn(move || {
338 let _ = match child_.wait() {
339 Ok(status) => {
340 let _l = guard.write().unwrap();
341 block_on_task(async move {
342 tx.send(CommandEvent::Terminated(TerminatedPayload {
343 code: status.code(),
344 #[cfg(windows)]
345 signal: None,
346 #[cfg(unix)]
347 signal: status.signal(),
348 }))
349 .await
350 })
351 }
352 Err(e) => {
353 let _l = guard.write().unwrap();
354 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
355 }
356 };
357 });
358
359 Ok((
360 rx,
361 CommandChild {
362 inner: child,
363 stdin_writer,
364 },
365 ))
366 }
367
368 pub async fn status(self) -> crate::Result<ExitStatus> {
382 let (mut rx, _child) = self.spawn()?;
383 let mut code = None;
384 #[allow(clippy::collapsible_match)]
385 while let Some(event) = rx.recv().await {
386 if let CommandEvent::Terminated(payload) = event {
387 code = payload.code;
388 }
389 }
390 Ok(ExitStatus { code })
391 }
392
393 pub async fn output(self) -> crate::Result<Output> {
409 let (mut rx, _child) = self.spawn()?;
410
411 let mut code = None;
412 let mut stdout = Vec::new();
413 let mut stderr = Vec::new();
414
415 while let Some(event) = rx.recv().await {
416 match event {
417 CommandEvent::Terminated(payload) => {
418 code = payload.code;
419 }
420 CommandEvent::Stdout(line) => {
421 stdout.extend(line);
422 stdout.push(NEWLINE_BYTE);
423 }
424 CommandEvent::Stderr(line) => {
425 stderr.extend(line);
426 stderr.push(NEWLINE_BYTE);
427 }
428 CommandEvent::Error(_) => {}
429 }
430 }
431 Ok(Output {
432 status: ExitStatus { code },
433 stdout,
434 stderr,
435 })
436 }
437}
438
439fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
440 mut reader: BufReader<PipeReader>,
441 tx: Sender<CommandEvent>,
442 wrapper: F,
443) {
444 loop {
445 let result = reader.fill_buf();
446 match result {
447 Ok(buf) => {
448 let length = buf.len();
449 if length == 0 {
450 break;
451 }
452 let tx_ = tx.clone();
453 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
454 reader.consume(length);
455 }
456 Err(e) => {
457 let tx_ = tx.clone();
458 let _ = block_on_task(
459 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
460 );
461 }
462 }
463 }
464}
465
466fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
467 mut reader: BufReader<PipeReader>,
468 tx: Sender<CommandEvent>,
469 wrapper: F,
470) {
471 loop {
472 let mut buf = Vec::new();
473 match tauri::utils::io::read_line(&mut reader, &mut buf) {
474 Ok(n) => {
475 if n == 0 {
476 break;
477 }
478 let tx_ = tx.clone();
479 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
480 }
481 Err(e) => {
482 let tx_ = tx.clone();
483 let _ = block_on_task(
484 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
485 );
486 break;
487 }
488 }
489 }
490}
491
492fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
493 tx: Sender<CommandEvent>,
494 guard: Arc<RwLock<()>>,
495 pipe_reader: PipeReader,
496 wrapper: F,
497 raw_out: bool,
498) {
499 spawn(move || {
500 let _lock = guard.read().unwrap();
501 let reader = BufReader::new(pipe_reader);
502
503 if raw_out {
504 read_raw_bytes(reader, tx, wrapper);
505 } else {
506 read_line(reader, tx, wrapper);
507 }
508 });
509}
510
511#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn relative_command_path_resolves() {
518 let cwd_parent = platform::current_exe()
519 .unwrap()
520 .parent()
521 .unwrap()
522 .parent() .unwrap()
524 .to_owned();
525 assert_eq!(
526 relative_command_path(Path::new("Tauri.Example")).unwrap(),
527 cwd_parent.join(if cfg!(windows) {
528 "Tauri.Example.exe"
529 } else {
530 "Tauri.Example"
531 })
532 );
533 assert_eq!(
534 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
535 cwd_parent.join(if cfg!(windows) {
536 "Tauri.Example.exe"
537 } else {
538 "Tauri.Example"
539 })
540 );
541 }
542
543 #[cfg(not(windows))]
544 #[test]
545 fn test_cmd_spawn_output() {
546 let cmd = Command::new("cat").args(["test/test.txt"]);
547 let (mut rx, _) = cmd.spawn().unwrap();
548
549 tauri::async_runtime::block_on(async move {
550 while let Some(event) = rx.recv().await {
551 match event {
552 CommandEvent::Terminated(payload) => {
553 assert_eq!(payload.code, Some(0));
554 }
555 CommandEvent::Stdout(line) => {
556 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
557 }
558 _ => {}
559 }
560 }
561 });
562 }
563
564 #[cfg(not(windows))]
565 #[test]
566 fn test_cmd_spawn_raw_output() {
567 let cmd = Command::new("cat").args(["test/test.txt"]);
568 let (mut rx, _) = cmd.spawn().unwrap();
569
570 tauri::async_runtime::block_on(async move {
571 while let Some(event) = rx.recv().await {
572 match event {
573 CommandEvent::Terminated(payload) => {
574 assert_eq!(payload.code, Some(0));
575 }
576 CommandEvent::Stdout(line) => {
577 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
578 }
579 _ => {}
580 }
581 }
582 });
583 }
584
585 #[cfg(not(windows))]
586 #[test]
587 fn test_cmd_spawn_fail() {
589 let cmd = Command::new("cat").args(["test/"]);
590 let (mut rx, _) = cmd.spawn().unwrap();
591
592 tauri::async_runtime::block_on(async move {
593 while let Some(event) = rx.recv().await {
594 match event {
595 CommandEvent::Terminated(payload) => {
596 assert_eq!(payload.code, Some(1));
597 }
598 CommandEvent::Stderr(line) => {
599 assert_eq!(
600 String::from_utf8(line).unwrap(),
601 "cat: test/: Is a directory\n"
602 );
603 }
604 _ => {}
605 }
606 }
607 });
608 }
609
610 #[cfg(not(windows))]
611 #[test]
612 fn test_cmd_spawn_raw_fail() {
614 let cmd = Command::new("cat").args(["test/"]);
615 let (mut rx, _) = cmd.spawn().unwrap();
616
617 tauri::async_runtime::block_on(async move {
618 while let Some(event) = rx.recv().await {
619 match event {
620 CommandEvent::Terminated(payload) => {
621 assert_eq!(payload.code, Some(1));
622 }
623 CommandEvent::Stderr(line) => {
624 assert_eq!(
625 String::from_utf8(line).unwrap(),
626 "cat: test/: Is a directory\n"
627 );
628 }
629 _ => {}
630 }
631 }
632 });
633 }
634
635 #[cfg(not(windows))]
636 #[test]
637 fn test_cmd_output_output() {
638 let cmd = Command::new("cat").args(["test/test.txt"]);
639 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
640
641 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
642 assert_eq!(
643 String::from_utf8(output.stdout).unwrap(),
644 "This is a test doc!\n"
645 );
646 }
647
648 #[cfg(not(windows))]
649 #[test]
650 fn test_cmd_output_output_fail() {
651 let cmd = Command::new("cat").args(["test/"]);
652 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
653
654 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
655 assert_eq!(
656 String::from_utf8(output.stderr).unwrap(),
657 "cat: test/: Is a directory\n\n"
658 );
659 }
660}