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    code: Option<i32>,
93}
94
95impl ExitStatus {
96    /// Returns the exit code of the process, if any.
97    pub fn code(&self) -> Option<i32> {
98        self.code
99    }
100
101    /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status.
102    pub fn success(&self) -> bool {
103        self.code == Some(0)
104    }
105}
106
107/// The output of a finished process.
108#[derive(Debug)]
109pub struct Output {
110    /// The status (exit code) of the process.
111    pub status: ExitStatus,
112    /// The data that the process wrote to stdout.
113    pub stdout: Vec<u8>,
114    /// The data that the process wrote to stderr.
115    pub stderr: Vec<u8>,
116}
117
118fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
119    match platform::current_exe()?.parent() {
120        #[cfg(windows)]
121        Some(exe_dir) => Ok(exe_dir.join(command).with_extension("exe")),
122        #[cfg(not(windows))]
123        Some(exe_dir) => Ok(exe_dir.join(command)),
124        None => Err(crate::Error::CurrentExeHasNoParent),
125    }
126}
127
128impl From<Command> for StdCommand {
129    fn from(cmd: Command) -> StdCommand {
130        cmd.cmd
131    }
132}
133
134impl Command {
135    pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
136        let mut command = StdCommand::new(program);
137
138        command.stdout(Stdio::piped());
139        command.stdin(Stdio::piped());
140        command.stderr(Stdio::piped());
141        #[cfg(windows)]
142        command.creation_flags(CREATE_NO_WINDOW);
143
144        Self {
145            cmd: command,
146            raw_out: false,
147        }
148    }
149
150    pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
151        Ok(Self::new(relative_command_path(program.as_ref())?))
152    }
153
154    /// Appends an argument to the command.
155    #[must_use]
156    pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
157        self.cmd.arg(arg);
158        self
159    }
160
161    /// Appends arguments to the command.
162    #[must_use]
163    pub fn args<I, S>(mut self, args: I) -> Self
164    where
165        I: IntoIterator<Item = S>,
166        S: AsRef<OsStr>,
167    {
168        self.cmd.args(args);
169        self
170    }
171
172    /// Clears the entire environment map for the child process.
173    #[must_use]
174    pub fn env_clear(mut self) -> Self {
175        self.cmd.env_clear();
176        self
177    }
178
179    /// Inserts or updates an explicit environment variable mapping.
180    #[must_use]
181    pub fn env<K, V>(mut self, key: K, value: V) -> Self
182    where
183        K: AsRef<OsStr>,
184        V: AsRef<OsStr>,
185    {
186        self.cmd.env(key, value);
187        self
188    }
189
190    /// Adds or updates multiple environment variable mappings.
191    #[must_use]
192    pub fn envs<I, K, V>(mut self, envs: I) -> Self
193    where
194        I: IntoIterator<Item = (K, V)>,
195        K: AsRef<OsStr>,
196        V: AsRef<OsStr>,
197    {
198        self.cmd.envs(envs);
199        self
200    }
201
202    /// Sets the working directory for the child process.
203    #[must_use]
204    pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
205        self.cmd.current_dir(current_dir);
206        self
207    }
208
209    /// Configures the reader to output bytes from the child process exactly as received
210    pub fn set_raw_out(mut self, raw_out: bool) -> Self {
211        self.raw_out = raw_out;
212        self
213    }
214
215    /// Spawns the command.
216    ///
217    /// # Examples
218    ///
219    /// ```rust,no_run
220    /// use tauri_plugin_shell::{process::CommandEvent, ShellExt};
221    /// tauri::Builder::default()
222    ///   .setup(|app| {
223    ///     let handle = app.handle().clone();
224    ///     tauri::async_runtime::spawn(async move {
225    ///       let (mut rx, mut child) = handle.shell().command("cargo")
226    ///         .args(["tauri", "dev"])
227    ///         .spawn()
228    ///         .expect("Failed to spawn cargo");
229    ///
230    ///       let mut i = 0;
231    ///       while let Some(event) = rx.recv().await {
232    ///         if let CommandEvent::Stdout(line) = event {
233    ///           println!("got: {}", String::from_utf8(line).unwrap());
234    ///           i += 1;
235    ///           if i == 4 {
236    ///             child.write("message from Rust\n".as_bytes()).unwrap();
237    ///             i = 0;
238    ///           }
239    ///         }
240    ///       }
241    ///     });
242    ///     Ok(())
243    /// });
244    /// ```
245    pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
246        let raw = self.raw_out;
247        let mut command: StdCommand = self.into();
248        let (stdout_reader, stdout_writer) = pipe()?;
249        let (stderr_reader, stderr_writer) = pipe()?;
250        let (stdin_reader, stdin_writer) = pipe()?;
251        command.stdout(stdout_writer);
252        command.stderr(stderr_writer);
253        command.stdin(stdin_reader);
254
255        let shared_child = SharedChild::spawn(&mut command)?;
256        let child = Arc::new(shared_child);
257        let child_ = child.clone();
258        let guard = Arc::new(RwLock::new(()));
259
260        let (tx, rx) = channel(1);
261
262        spawn_pipe_reader(
263            tx.clone(),
264            guard.clone(),
265            stdout_reader,
266            CommandEvent::Stdout,
267            raw,
268        );
269        spawn_pipe_reader(
270            tx.clone(),
271            guard.clone(),
272            stderr_reader,
273            CommandEvent::Stderr,
274            raw,
275        );
276
277        spawn(move || {
278            let _ = match child_.wait() {
279                Ok(status) => {
280                    let _l = guard.write().unwrap();
281                    block_on_task(async move {
282                        tx.send(CommandEvent::Terminated(TerminatedPayload {
283                            code: status.code(),
284                            #[cfg(windows)]
285                            signal: None,
286                            #[cfg(unix)]
287                            signal: status.signal(),
288                        }))
289                        .await
290                    })
291                }
292                Err(e) => {
293                    let _l = guard.write().unwrap();
294                    block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
295                }
296            };
297        });
298
299        Ok((
300            rx,
301            CommandChild {
302                inner: child,
303                stdin_writer,
304            },
305        ))
306    }
307
308    /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
309    /// Stdin, stdout and stderr are ignored.
310    ///
311    /// # Examples
312    /// ```rust,no_run
313    /// use tauri_plugin_shell::ShellExt;
314    /// tauri::Builder::default()
315    ///   .setup(|app| {
316    ///     let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() });
317    ///     println!("`which` finished with status: {:?}", status.code());
318    ///     Ok(())
319    ///   });
320    /// ```
321    pub async fn status(self) -> crate::Result<ExitStatus> {
322        let (mut rx, _child) = self.spawn()?;
323        let mut code = None;
324        #[allow(clippy::collapsible_match)]
325        while let Some(event) = rx.recv().await {
326            if let CommandEvent::Terminated(payload) = event {
327                code = payload.code;
328            }
329        }
330        Ok(ExitStatus { code })
331    }
332
333    /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
334    /// Stdin is ignored.
335    ///
336    /// # Examples
337    ///
338    /// ```rust,no_run
339    /// use tauri_plugin_shell::ShellExt;
340    /// tauri::Builder::default()
341    ///   .setup(|app| {
342    ///     let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() });
343    ///     assert!(output.status.success());
344    ///     assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
345    ///     Ok(())
346    ///   });
347    /// ```
348    pub async fn output(self) -> crate::Result<Output> {
349        let (mut rx, _child) = self.spawn()?;
350
351        let mut code = None;
352        let mut stdout = Vec::new();
353        let mut stderr = Vec::new();
354
355        while let Some(event) = rx.recv().await {
356            match event {
357                CommandEvent::Terminated(payload) => {
358                    code = payload.code;
359                }
360                CommandEvent::Stdout(line) => {
361                    stdout.extend(line);
362                    stdout.push(NEWLINE_BYTE);
363                }
364                CommandEvent::Stderr(line) => {
365                    stderr.extend(line);
366                    stderr.push(NEWLINE_BYTE);
367                }
368                CommandEvent::Error(_) => {}
369            }
370        }
371        Ok(Output {
372            status: ExitStatus { code },
373            stdout,
374            stderr,
375        })
376    }
377}
378
379fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
380    mut reader: BufReader<PipeReader>,
381    tx: Sender<CommandEvent>,
382    wrapper: F,
383) {
384    loop {
385        let result = reader.fill_buf();
386        match result {
387            Ok(buf) => {
388                let length = buf.len();
389                if length == 0 {
390                    break;
391                }
392                let tx_ = tx.clone();
393                let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
394                reader.consume(length);
395            }
396            Err(e) => {
397                let tx_ = tx.clone();
398                let _ = block_on_task(
399                    async move { tx_.send(CommandEvent::Error(e.to_string())).await },
400                );
401            }
402        }
403    }
404}
405
406fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
407    mut reader: BufReader<PipeReader>,
408    tx: Sender<CommandEvent>,
409    wrapper: F,
410) {
411    loop {
412        let mut buf = Vec::new();
413        match tauri::utils::io::read_line(&mut reader, &mut buf) {
414            Ok(n) => {
415                if n == 0 {
416                    break;
417                }
418                let tx_ = tx.clone();
419                let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
420            }
421            Err(e) => {
422                let tx_ = tx.clone();
423                let _ = block_on_task(
424                    async move { tx_.send(CommandEvent::Error(e.to_string())).await },
425                );
426                break;
427            }
428        }
429    }
430}
431
432fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
433    tx: Sender<CommandEvent>,
434    guard: Arc<RwLock<()>>,
435    pipe_reader: PipeReader,
436    wrapper: F,
437    raw_out: bool,
438) {
439    spawn(move || {
440        let _lock = guard.read().unwrap();
441        let reader = BufReader::new(pipe_reader);
442
443        if raw_out {
444            read_raw_bytes(reader, tx, wrapper);
445        } else {
446            read_line(reader, tx, wrapper);
447        }
448    });
449}
450
451// tests for the commands functions.
452#[cfg(test)]
453mod tests {
454    #[cfg(not(windows))]
455    use super::*;
456
457    #[cfg(not(windows))]
458    #[test]
459    fn test_cmd_spawn_output() {
460        let cmd = Command::new("cat").args(["test/test.txt"]);
461        let (mut rx, _) = cmd.spawn().unwrap();
462
463        tauri::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(0));
468                    }
469                    CommandEvent::Stdout(line) => {
470                        assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
471                    }
472                    _ => {}
473                }
474            }
475        });
476    }
477
478    #[cfg(not(windows))]
479    #[test]
480    fn test_cmd_spawn_raw_output() {
481        let cmd = Command::new("cat").args(["test/test.txt"]);
482        let (mut rx, _) = cmd.spawn().unwrap();
483
484        tauri::async_runtime::block_on(async move {
485            while let Some(event) = rx.recv().await {
486                match event {
487                    CommandEvent::Terminated(payload) => {
488                        assert_eq!(payload.code, Some(0));
489                    }
490                    CommandEvent::Stdout(line) => {
491                        assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
492                    }
493                    _ => {}
494                }
495            }
496        });
497    }
498
499    #[cfg(not(windows))]
500    #[test]
501    // test the failure case
502    fn test_cmd_spawn_fail() {
503        let cmd = Command::new("cat").args(["test/"]);
504        let (mut rx, _) = cmd.spawn().unwrap();
505
506        tauri::async_runtime::block_on(async move {
507            while let Some(event) = rx.recv().await {
508                match event {
509                    CommandEvent::Terminated(payload) => {
510                        assert_eq!(payload.code, Some(1));
511                    }
512                    CommandEvent::Stderr(line) => {
513                        assert_eq!(
514                            String::from_utf8(line).unwrap(),
515                            "cat: test/: Is a directory\n"
516                        );
517                    }
518                    _ => {}
519                }
520            }
521        });
522    }
523
524    #[cfg(not(windows))]
525    #[test]
526    // test the failure case (raw encoding)
527    fn test_cmd_spawn_raw_fail() {
528        let cmd = Command::new("cat").args(["test/"]);
529        let (mut rx, _) = cmd.spawn().unwrap();
530
531        tauri::async_runtime::block_on(async move {
532            while let Some(event) = rx.recv().await {
533                match event {
534                    CommandEvent::Terminated(payload) => {
535                        assert_eq!(payload.code, Some(1));
536                    }
537                    CommandEvent::Stderr(line) => {
538                        assert_eq!(
539                            String::from_utf8(line).unwrap(),
540                            "cat: test/: Is a directory\n"
541                        );
542                    }
543                    _ => {}
544                }
545            }
546        });
547    }
548
549    #[cfg(not(windows))]
550    #[test]
551    fn test_cmd_output_output() {
552        let cmd = Command::new("cat").args(["test/test.txt"]);
553        let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
554
555        assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
556        assert_eq!(
557            String::from_utf8(output.stdout).unwrap(),
558            "This is a test doc!\n"
559        );
560    }
561
562    #[cfg(not(windows))]
563    #[test]
564    fn test_cmd_output_output_fail() {
565        let cmd = Command::new("cat").args(["test/"]);
566        let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
567
568        assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
569        assert_eq!(
570            String::from_utf8(output.stderr).unwrap(),
571            "cat: test/: Is a directory\n\n"
572        );
573    }
574}