166 lines
5.3 KiB
Rust
166 lines
5.3 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//! 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 tauri::{
|
|
plugin::{Builder, TauriPlugin},
|
|
AppHandle, Manager, RunEvent, Runtime,
|
|
};
|
|
|
|
mod commands;
|
|
mod config;
|
|
mod error;
|
|
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
|
|
#[allow(deprecated)]
|
|
pub mod open;
|
|
pub mod process;
|
|
mod scope;
|
|
mod scope_entry;
|
|
|
|
pub use error::Error;
|
|
type Result<T> = std::result::Result<T, Error>;
|
|
|
|
#[cfg(mobile)]
|
|
use tauri::plugin::PluginHandle;
|
|
#[cfg(target_os = "android")]
|
|
const PLUGIN_IDENTIFIER: &str = "app.tauri.shell";
|
|
#[cfg(target_os = "ios")]
|
|
tauri::ios_plugin_binding!(init_plugin_shell);
|
|
|
|
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
|
|
|
|
pub struct Shell<R: Runtime> {
|
|
#[allow(dead_code)]
|
|
app: AppHandle<R>,
|
|
#[cfg(mobile)]
|
|
mobile_plugin_handle: PluginHandle<R>,
|
|
open_scope: scope::OpenScope,
|
|
children: ChildStore,
|
|
}
|
|
|
|
impl<R: Runtime> Shell<R> {
|
|
/// Creates a new Command for launching the given program.
|
|
pub fn command(&self, program: impl AsRef<OsStr>) -> 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<Path>) -> Result<Command> {
|
|
Command::new_sidecar(program)
|
|
}
|
|
|
|
/// Open a (url) path with a default or specific browser opening program.
|
|
///
|
|
/// See [`crate::open::open`] for how it handles security-related measures.
|
|
#[cfg(desktop)]
|
|
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
|
|
#[allow(deprecated)]
|
|
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
|
|
open::open(None, path.into(), with)
|
|
}
|
|
|
|
/// Open a (url) path with a default or specific browser opening program.
|
|
///
|
|
/// See [`crate::open::open`] for how it handles security-related measures.
|
|
#[cfg(mobile)]
|
|
#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
|
|
pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
|
|
self.mobile_plugin_handle
|
|
.run_mobile_plugin("open", path.into())
|
|
.map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
pub trait ShellExt<R: Runtime> {
|
|
fn shell(&self) -> &Shell<R>;
|
|
}
|
|
|
|
impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
|
|
fn shell(&self) -> &Shell<R> {
|
|
self.state::<Shell<R>>().inner()
|
|
}
|
|
}
|
|
|
|
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
|
Builder::<R, Option<config::Config>>::new("shell")
|
|
.js_init_script(include_str!("init-iife.js").to_string())
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::execute,
|
|
commands::spawn,
|
|
commands::stdin_write,
|
|
commands::kill,
|
|
commands::open
|
|
])
|
|
.setup(|app, api| {
|
|
let default_config = config::Config::default();
|
|
let config = api.config().as_ref().unwrap_or(&default_config);
|
|
|
|
#[cfg(target_os = "android")]
|
|
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?;
|
|
#[cfg(target_os = "ios")]
|
|
let handle = api.register_ios_plugin(init_plugin_shell)?;
|
|
|
|
app.manage(Shell {
|
|
app: app.clone(),
|
|
children: Default::default(),
|
|
open_scope: open_scope(&config.open),
|
|
|
|
#[cfg(mobile)]
|
|
mobile_plugin_handle: handle,
|
|
});
|
|
Ok(())
|
|
})
|
|
.on_event(|app, event| {
|
|
if let RunEvent::Exit = event {
|
|
let shell = app.state::<Shell<R>>();
|
|
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 open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
|
|
let shell_scope_open = match open {
|
|
config::ShellAllowlistOpen::Flag(false) => None,
|
|
// we want to add a basic regex validation even if the config is not set
|
|
config::ShellAllowlistOpen::Unset | config::ShellAllowlistOpen::Flag(true) => {
|
|
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
|
|
}
|
|
config::ShellAllowlistOpen::Validate(validator) => {
|
|
let regex = format!("^{validator}$");
|
|
let validator =
|
|
Regex::new(®ex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
|
|
Some(validator)
|
|
}
|
|
};
|
|
|
|
scope::OpenScope {
|
|
open: shell_scope_open,
|
|
}
|
|
}
|