Skip to main content

tauri_plugin_shell/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application.
6
7#![doc(
8    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12use std::{
13    collections::HashMap,
14    ffi::OsStr,
15    path::Path,
16    sync::{Arc, Mutex},
17};
18
19use process::{Command, CommandChild};
20use regex::Regex;
21use tauri::{
22    AppHandle, Manager, RunEvent, Runtime,
23    plugin::{Builder, TauriPlugin},
24};
25
26mod commands;
27mod config;
28mod error;
29#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
30#[allow(deprecated)]
31pub mod open;
32/// Types and helpers to spawn and interact with child processes.
33pub mod process;
34mod scope;
35mod scope_entry;
36
37pub use error::Error;
38type Result<T> = std::result::Result<T, Error>;
39
40#[cfg(mobile)]
41use tauri::plugin::PluginHandle;
42#[cfg(target_os = "android")]
43const PLUGIN_IDENTIFIER: &str = "app.tauri.shell";
44#[cfg(target_os = "ios")]
45tauri::ios_plugin_binding!(init_plugin_shell);
46
47type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
48
49/// Access to the shell APIs.
50///
51/// Get an instance of this type with [`ShellExt::shell`].
52pub struct Shell<R: Runtime> {
53    #[allow(dead_code)]
54    app: AppHandle<R>,
55    #[cfg(mobile)]
56    mobile_plugin_handle: PluginHandle<R>,
57    open_scope: scope::OpenScope,
58    children: ChildStore,
59}
60
61impl<R: Runtime> Shell<R> {
62    /// Creates a new Command for launching the given program.
63    pub fn command(&self, program: impl AsRef<OsStr>) -> Command {
64        Command::new(program)
65    }
66
67    /// Creates a new Command for launching the given sidecar program.
68    ///
69    /// A sidecar program is a embedded external binary in order to make your application work
70    /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc).
71    pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> {
72        Command::new_sidecar(program)
73    }
74
75    /// Open a (url) path with a default or specific browser opening program.
76    ///
77    /// See [`crate::open::open`] for how it handles security-related measures.
78    #[cfg(desktop)]
79    #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
80    #[allow(deprecated)]
81    pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
82        open::open(None, path.into(), with)
83    }
84
85    /// Open a (url) path with a default or specific browser opening program.
86    ///
87    /// See [`crate::open::open`] for how it handles security-related measures.
88    #[cfg(mobile)]
89    #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
90    #[allow(deprecated)]
91    pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
92        self.mobile_plugin_handle
93            .run_mobile_plugin("open", path.into())
94            .map_err(Into::into)
95    }
96}
97
98/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`],
99/// [`tauri::Webview`] and [`tauri::Window`] to access the shell APIs.
100pub trait ShellExt<R: Runtime> {
101    /// Gets the shell APIs.
102    ///
103    /// # Examples
104    ///
105    /// ```no_run
106    /// use tauri_plugin_shell::ShellExt;
107    ///
108    /// async fn run_echo<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
109    ///     let output = app.shell().command("echo").args(["hello"]).output().await.unwrap();
110    ///     println!("{}", String::from_utf8_lossy(&output.stdout));
111    /// }
112    /// ```
113    fn shell(&self) -> &Shell<R>;
114}
115
116impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
117    fn shell(&self) -> &Shell<R> {
118        self.state::<Shell<R>>().inner()
119    }
120}
121
122/// Initializes the shell plugin.
123///
124/// The plugin state can be accessed with [`ShellExt::shell`],
125/// and all spawned child processes are killed when the application exits.
126///
127/// # Examples
128///
129/// ```no_run
130/// fn setup<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
131///     builder.plugin(tauri_plugin_shell::init())
132/// }
133/// ```
134pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
135    Builder::<R, Option<config::Config>>::new("shell")
136        .js_init_script(include_str!("init-iife.js").to_string())
137        .invoke_handler(tauri::generate_handler![
138            commands::execute,
139            commands::spawn,
140            commands::stdin_write,
141            commands::kill,
142            commands::open
143        ])
144        .setup(|app, api| {
145            let default_config = config::Config::default();
146            let config = api.config().as_ref().unwrap_or(&default_config);
147
148            #[cfg(target_os = "android")]
149            let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?;
150            #[cfg(target_os = "ios")]
151            let handle = api.register_ios_plugin(init_plugin_shell)?;
152
153            app.manage(Shell {
154                app: app.clone(),
155                children: Default::default(),
156                open_scope: open_scope(&config.open),
157
158                #[cfg(mobile)]
159                mobile_plugin_handle: handle,
160            });
161            Ok(())
162        })
163        .on_event(|app, event| {
164            if let RunEvent::Exit = event {
165                let shell = app.state::<Shell<R>>();
166                let children = {
167                    let mut lock = shell.children.lock().unwrap();
168                    std::mem::take(&mut *lock)
169                };
170                for child in children.into_values() {
171                    let _ = child.kill();
172                }
173            }
174        })
175        .build()
176}
177
178fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
179    let shell_scope_open = match open {
180        config::ShellAllowlistOpen::Flag(false) => None,
181        // we want to add a basic regex validation even if the config is not set
182        config::ShellAllowlistOpen::Unset | config::ShellAllowlistOpen::Flag(true) => {
183            Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
184        }
185        config::ShellAllowlistOpen::Validate(validator) => {
186            let regex = format!("^{validator}$");
187            let validator =
188                Regex::new(&regex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
189            Some(validator)
190        }
191    };
192
193    scope::OpenScope {
194        open: shell_scope_open,
195    }
196}