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, 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    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 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    /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
369    /// Stdin, stdout and stderr are ignored.
370    ///
371    /// # Examples
372    /// ```rust,no_run
373    /// use tauri_plugin_shell::ShellExt;
374    /// tauri::Builder::default()
375    ///   .setup(|app| {
376    ///     let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() });
377    ///     println!("`which` finished with status: {:?}", status.code());
378    ///     Ok(())
379    ///   });
380    /// ```
381    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    /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
394    /// Stdin is ignored.
395    ///
396    /// # Examples
397    ///
398    /// ```rust,no_run
399    /// use tauri_plugin_shell::ShellExt;
400    /// tauri::Builder::default()
401    ///   .setup(|app| {
402    ///     let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() });
403    ///     assert!(output.status.success());
404    ///     assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
405    ///     Ok(())
406    ///   });
407    /// ```
408    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// tests for the commands functions.
512#[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() // Go up once more to get out of the "deps" directory
523            .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    // test the failure case
588    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    // test the failure case (raw encoding)
613    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}