// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/shell) //! //! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. #![doc( html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] use std::{ collections::HashMap, ffi::OsStr, path::Path, sync::{Arc, Mutex}, }; use process::{Command, CommandChild}; use regex::Regex; use scope::{Scope, ScopeAllowedCommand, ScopeConfig}; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, RunEvent, Runtime, }; mod commands; mod config; mod error; mod open; pub mod process; mod scope; use config::{Config, ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope}; pub use error::Error; type Result = std::result::Result; type ChildStore = Arc>>; pub struct Shell { #[allow(dead_code)] app: AppHandle, scope: Scope, children: ChildStore, } impl Shell { /// Creates a new Command for launching the given program. pub fn command(&self, program: impl AsRef) -> Command { Command::new(program) } /// Creates a new Command for launching the given sidecar program. /// /// A sidecar program is a embedded external binary in order to make your application work /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc). pub fn sidecar(&self, program: impl AsRef) -> Result { Command::new_sidecar(program) } /// Open a (url) path with a default or specific browser opening program. /// /// See [`crate::api::shell::open`] for how it handles security-related measures. pub fn open(&self, path: impl Into, with: Option) -> Result<()> { open::open(&self.scope, path.into(), with).map_err(Into::into) } } pub trait ShellExt { fn shell(&self) -> &Shell; } impl> ShellExt for T { fn shell(&self) -> &Shell { self.state::>().inner() } } pub fn init() -> TauriPlugin> { let mut init_script = include_str!("init.js").to_string(); init_script.push_str(include_str!("api-iife.js")); Builder::>::new("shell") .js_init_script(init_script) .invoke_handler(tauri::generate_handler![ commands::execute, commands::stdin_write, commands::kill, commands::open ]) .setup(|app, api| { let default_config = Config::default(); let config = api.config().as_ref().unwrap_or(&default_config); app.manage(Shell { app: app.clone(), children: Default::default(), scope: Scope::new(app, shell_scope(config.scope.clone(), &config.open)), }); Ok(()) }) .on_event(|app, event| { if let RunEvent::Exit = event { let shell = app.state::>(); let children = { let mut lock = shell.children.lock().unwrap(); std::mem::take(&mut *lock) }; for child in children.into_values() { let _ = child.kill(); } } }) .build() } fn shell_scope(scope: ShellAllowlistScope, open: &ShellAllowlistOpen) -> ScopeConfig { let shell_scopes = get_allowed_clis(scope); let shell_scope_open = match open { ShellAllowlistOpen::Flag(false) => None, ShellAllowlistOpen::Flag(true) => { Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap()) } ShellAllowlistOpen::Validate(validator) => { let validator = Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}")); Some(validator) } }; ScopeConfig { open: shell_scope_open, scopes: shell_scopes, } } fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap { scope .0 .into_iter() .map(|scope| { let args = match scope.args { ShellAllowedArgs::Flag(true) => None, ShellAllowedArgs::Flag(false) => Some(Vec::new()), ShellAllowedArgs::List(list) => { let list = list.into_iter().map(|arg| match arg { ShellAllowedArg::Fixed(fixed) => scope::ScopeAllowedArg::Fixed(fixed), ShellAllowedArg::Var { validator } => { let validator = Regex::new(&validator) .unwrap_or_else(|e| panic!("invalid regex {validator}: {e}")); scope::ScopeAllowedArg::Var { validator } } }); Some(list.collect()) } }; ( scope.name, ScopeAllowedCommand { command: scope.command, args, sidecar: scope.sidecar, }, ) }) .collect() }