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