Skip to main content

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,
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/// 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    let exe_path = platform::current_exe()?;
122
123    let exe_dir = exe_path
124        .parent()
125        .ok_or(crate::Error::CurrentExeHasNoParent)?;
126
127    // If a test is being run, the executable is in the "deps" directory, so we need to go up one level.
128    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            // do not use with_extension to retain dots in the command filename
141            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    /// Appends an argument to the command.
186    #[must_use]
187    pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
188        self.cmd.arg(arg);
189        self
190    }
191
192    /// Appends arguments to the command.
193    #[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    /// Clears the entire environment map for the child process.
204    #[must_use]
205    pub fn env_clear(mut self) -> Self {
206        self.cmd.env_clear();
207        self
208    }
209
210    /// Inserts or updates an explicit environment variable mapping.
211    #[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    /// Adds or updates multiple environment variable mappings.
222    #[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    /// Sets the working directory for the child process.
234    #[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    /// Configures the reader to output bytes from the child process exactly as received
241    pub fn set_raw_out(mut self, raw_out: bool) -> Self {
242        self.raw_out = raw_out;
243        self
244    }
245
246    /// Spawns the command.
247    ///
248    /// # Examples
249    ///
250    /// ```rust,no_run
251    /// use tauri_plugin_shell::{process::CommandEvent, ShellExt};
252    /// tauri::Builder::default()
253    ///   .setup(|app| {
254    ///     let handle = app.handle().clone();
255    ///     tauri::async_runtime::spawn(async move {
256    ///       let (mut rx, mut child) = handle
257    ///         .shell()
258    ///         .command("cargo")
259    ///         .args(["tauri", "dev"])
260    ///         .spawn()
261    ///         .expect("Failed to spawn cargo");
262    ///
263    ///       let mut i = 0;
264    ///       while let Some(event) = rx.recv().await {
265    ///         if let CommandEvent::Stdout(line) = event {
266    ///           println!("got: {}", String::from_utf8(line).unwrap());
267    ///           i += 1;
268    ///           if i == 4 {
269    ///             child.write("message from Rust\n".as_bytes()).unwrap();
270    ///             i = 0;
271    ///           }
272    ///         }
273    ///       }
274    ///     });
275    ///     Ok(())
276    ///   });
277    /// ```
278    ///
279    /// Depending on the command you spawn, it might output in a specific encoding, to parse the output lines in this case:
280    ///
281    /// ```rust,no_run
282    /// use tauri_plugin_shell::{process::{CommandEvent, Encoding}, ShellExt};
283    /// tauri::Builder::default()
284    ///   .setup(|app| {
285    ///     let handle = app.handle().clone();
286    ///     tauri::async_runtime::spawn(async move {
287    ///       let (mut rx, mut child) = handle
288    ///         .shell()
289    ///         .command("some-program")
290    ///         .arg("some-arg")
291    ///         .spawn()
292    ///         .expect("Failed to spawn some-program");
293    ///
294    ///       let encoding = Encoding::for_label(b"windows-1252").unwrap();
295    ///       while let Some(event) = rx.recv().await {
296    ///         if let CommandEvent::Stdout(line) = event {
297    ///           let (decoded, _, _) = encoding.decode(&line);
298    ///           println!("got: {decoded}");
299    ///         }
300    ///       }
301    ///     });
302    ///     Ok(())
303    ///   });
304    /// ```
305    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            // `Terminated` must be the last event: wait for the readers to drain the pipes,
326            // since a short-lived child can exit before they have read anything.
327            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    /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
356    /// Stdin, stdout and stderr are ignored.
357    ///
358    /// # Examples
359    /// ```rust,no_run
360    /// use tauri_plugin_shell::ShellExt;
361    /// tauri::Builder::default()
362    ///   .setup(|app| {
363    ///     let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() });
364    ///     println!("`which` finished with status: {:?}", status.code());
365    ///     Ok(())
366    ///   });
367    /// ```
368    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    /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
381    /// Stdin is ignored.
382    ///
383    /// # Examples
384    ///
385    /// ```rust,no_run
386    /// use tauri_plugin_shell::ShellExt;
387    /// tauri::Builder::default()
388    ///   .setup(|app| {
389    ///     let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() });
390    ///     assert!(output.status.success());
391    ///     assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
392    ///     Ok(())
393    ///   });
394    /// ```
395    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// tests for the commands functions.
495#[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() // Go up once more to get out of the "deps" directory
506            .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    // test the failure case
571    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    // test the failure case (raw encoding)
596    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}