tauri_plugin_shell/process/
mod.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use 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/// Payload for the [`CommandEvent::Terminated`] command event.
32#[derive(Debug, Clone, Serialize)]
33pub struct TerminatedPayload {
34    /// Exit code of the process.
35    pub code: Option<i32>,
36    /// If the process was terminated by a signal, represents that signal.
37    pub signal: Option<i32>,
38}
39
40/// A event sent to the command callback.
41#[derive(Debug, Clone)]
42#[non_exhaustive]
43pub enum CommandEvent {
44    /// If configured for raw output, all bytes written to stderr.
45    /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found.
46    Stderr(Vec<u8>),
47    /// If configured for raw output, all bytes written to stdout.
48    /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found.
49    Stdout(Vec<u8>),
50    /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to a UTF-8 string.
51    Error(String),
52    /// Command process terminated.
53    Terminated(TerminatedPayload),
54}
55
56/// The type to spawn commands.
57#[derive(Debug)]
58pub struct Command {
59    cmd: StdCommand,
60    raw_out: bool,
61}
62
63/// Spawned child process.
64#[derive(Debug)]
65pub struct CommandChild {
66    inner: Arc<SharedChild>,
67    stdin_writer: PipeWriter,
68}
69
70impl CommandChild {
71    /// Writes to process stdin.
72    pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
73        self.stdin_writer.write_all(buf)?;
74        Ok(())
75    }
76
77    /// Sends a kill signal to the child.
78    pub fn kill(self) -> crate::Result<()> {
79        self.inner.kill()?;
80        Ok(())
81    }
82
83    /// Returns the process pid.
84    pub fn pid(&self) -> u32 {
85        self.inner.id()
86    }
87}
88
89/// Describes the result of a process after it has terminated.
90#[derive(Debug)]
91pub struct ExitStatus {
92    // This field is intentionally left private.
93    // See: https://github.com/tauri-apps/plugins-workspace/pull/3115.
94    code: Option<i32>,
95}
96
97impl ExitStatus {
98    /// Returns the exit code of the process, if any.
99    pub fn code(&self) -> Option<i32> {
100        self.code
101    }
102
103    /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status.
104    pub fn success(&self) -> bool {
105        self.code == Some(0)
106    }
107}
108
109/// The output of a finished process.
110#[derive(Debug)]
111pub struct Output {
112    /// The status (exit code) of the process.
113    pub status: ExitStatus,
114    /// The data that the process wrote to stdout.
115    pub stdout: Vec<u8>,
116    /// The data that the process wrote to stderr.
117    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                // do not use with_extension to retain dots in the command filename
128                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    /// Appends an argument to the command.
175    #[must_use]
176    pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
177        self.cmd.arg(arg);
178        self
179    }
180
181    /// Appends arguments to the command.
182    #[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    /// Clears the entire environment map for the child process.
193    #[must_use]
194    pub fn env_clear(mut self) -> Self {
195        self.cmd.env_clear();
196        self
197    }
198
199    /// Inserts or updates an explicit environment variable mapping.
200    #[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    /// Adds or updates multiple environment variable mappings.
211    #[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    /// Sets the working directory for the child process.
223    #[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    /// Configures the reader to output bytes from the child process exactly as received
230    pub fn set_raw_out(mut self, raw_out: bool) -> Self {
231        self.raw_out = raw_out;
232        self
233    }
234
235    /// Spawns the command.
236    ///
237    /// # Examples
238    ///
239    /// ```rust,no_run
240    /// use tauri_plugin_shell::{process::CommandEvent, ShellExt};
241    /// tauri::Builder::default()
242    ///   .setup(|app| {
243    ///     let handle = app.handle().clone();
244    ///     tauri::async_runtime::spawn(async move {
245    ///       let (mut rx, mut child) = handle
246    ///         .shell()
247    ///         .command("cargo")
248    ///         .args(["tauri", "dev"])
249    ///         .spawn()
250    ///         .expect("Failed to spawn cargo");
251    ///
252    ///       let mut i = 0;
253    ///       while let Some(event) = rx.recv().await {
254    ///         if let CommandEvent::Stdout(line) = event {
255    ///           println!("got: {}", String::from_utf8(line).unwrap());
256    ///           i += 1;
257    ///           if i == 4 {
258    ///             child.write("message from Rust\n".as_bytes()).unwrap();
259    ///             i = 0;
260    ///           }
261    ///         }
262    ///       }
263    ///     });
264    ///     Ok(())
265    ///   });
266    /// ```
267    ///
268    /// Depending on the command you spawn, it might output in a specific encoding, to parse the output lines in this case:
269    ///
270    /// ```rust,no_run
271    /// use tauri_plugin_shell::{process::{CommandEvent, Encoding}, ShellExt};
272    /// tauri::Builder::default()
273    ///   .setup(|app| {
274    ///     let handle = app.handle().clone();
275    ///     tauri::async_runtime::spawn(async move {
276    ///       let (mut rx, mut child) = handle
277    ///         .shell()
278    ///         .command("some-program")
279    ///         .arg("some-arg")
280    ///         .spawn()
281    ///         .expect("Failed to spawn some-program");
282    ///
283    ///       let encoding = Encoding::for_label(b"windows-1252").unwrap();
284    ///       while let Some(event) = rx.recv().await {
285    ///         if let CommandEvent::Stdout(line) = event {
286    ///           let (decoded, _, _) = encoding.decode(&line);
287    ///           println!("got: {decoded}");
288    ///         }
289    ///       }
290    ///     });
291    ///     Ok(())
292    ///   });
293    /// ```
294    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    /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
358    /// Stdin, stdout and stderr are ignored.
359    ///
360    /// # Examples
361    /// ```rust,no_run
362    /// use tauri_plugin_shell::ShellExt;
363    /// tauri::Builder::default()
364    ///   .setup(|app| {
365    ///     let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() });
366    ///     println!("`which` finished with status: {:?}", status.code());
367    ///     Ok(())
368    ///   });
369    /// ```
370    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    /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
383    /// Stdin is ignored.
384    ///
385    /// # Examples
386    ///
387    /// ```rust,no_run
388    /// use tauri_plugin_shell::ShellExt;
389    /// tauri::Builder::default()
390    ///   .setup(|app| {
391    ///     let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() });
392    ///     assert!(output.status.success());
393    ///     assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
394    ///     Ok(())
395    ///   });
396    /// ```
397    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// tests for the commands functions.
501#[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    // test the failure case
575    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    // test the failure case (raw encoding)
600    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}