mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-05-25 13:17:47 +02:00
feat(shell): add plugin (#327)
This commit is contained in:
committed by
GitHub
parent
89fb40caac
commit
8ed00adaa0
@@ -0,0 +1,211 @@
|
||||
use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{api::ipc::CallbackFn, Manager, Runtime, State, Window};
|
||||
|
||||
use crate::{
|
||||
open::Program,
|
||||
process::{CommandEvent, TerminatedPayload},
|
||||
scope::ExecuteArgs,
|
||||
Shell,
|
||||
};
|
||||
|
||||
type ChildId = u32;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "event", content = "payload")]
|
||||
#[non_exhaustive]
|
||||
enum JSCommandEvent {
|
||||
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
|
||||
Stderr(Buffer),
|
||||
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
|
||||
Stdout(Buffer),
|
||||
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string.
|
||||
Error(String),
|
||||
/// Command process terminated.
|
||||
Terminated(TerminatedPayload),
|
||||
}
|
||||
|
||||
fn get_event_buffer(line: Vec<u8>, encoding: EncodingWrapper) -> Result<Buffer, FromUtf8Error> {
|
||||
match encoding {
|
||||
EncodingWrapper::Text(character_encoding) => match character_encoding {
|
||||
Some(encoding) => Ok(Buffer::Text(
|
||||
encoding.decode_with_bom_removal(&line).0.into(),
|
||||
)),
|
||||
None => String::from_utf8(line).map(Buffer::Text),
|
||||
},
|
||||
EncodingWrapper::Raw => Ok(Buffer::Raw(line)),
|
||||
}
|
||||
}
|
||||
|
||||
impl JSCommandEvent {
|
||||
pub fn new(event: CommandEvent, encoding: EncodingWrapper) -> Self {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => JSCommandEvent::Terminated(payload),
|
||||
CommandEvent::Error(error) => JSCommandEvent::Error(error),
|
||||
CommandEvent::Stderr(line) => get_event_buffer(line, encoding)
|
||||
.map(JSCommandEvent::Stderr)
|
||||
.unwrap_or_else(|e| JSCommandEvent::Error(e.to_string())),
|
||||
CommandEvent::Stdout(line) => get_event_buffer(line, encoding)
|
||||
.map(JSCommandEvent::Stdout)
|
||||
.unwrap_or_else(|e| JSCommandEvent::Error(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Buffer {
|
||||
Text(String),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum EncodingWrapper {
|
||||
Raw,
|
||||
Text(Option<&'static Encoding>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommandOptions {
|
||||
#[serde(default)]
|
||||
sidecar: bool,
|
||||
cwd: Option<PathBuf>,
|
||||
// by default we don't add any env variables to the spawned process
|
||||
// but the env is an `Option` so when it's `None` we clear the env.
|
||||
#[serde(default = "default_env")]
|
||||
env: Option<HashMap<String, String>>,
|
||||
// Character encoding for stdout/stderr
|
||||
encoding: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn default_env() -> Option<HashMap<String, String>> {
|
||||
Some(HashMap::default())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn execute<R: Runtime>(
|
||||
window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
program: String,
|
||||
args: ExecuteArgs,
|
||||
on_event_fn: CallbackFn,
|
||||
options: CommandOptions,
|
||||
) -> crate::Result<ChildId> {
|
||||
let mut command = if options.sidecar {
|
||||
let program = PathBuf::from(program);
|
||||
let program_as_string = program.display().to_string();
|
||||
let program_no_ext_as_string = program.with_extension("").display().to_string();
|
||||
let configured_sidecar = window
|
||||
.config()
|
||||
.tauri
|
||||
.bundle
|
||||
.external_bin
|
||||
.as_ref()
|
||||
.and_then(|bins| {
|
||||
bins.iter()
|
||||
.find(|b| b == &&program_as_string || b == &&program_no_ext_as_string)
|
||||
})
|
||||
.cloned();
|
||||
if let Some(sidecar) = configured_sidecar {
|
||||
shell
|
||||
.scope
|
||||
.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)?
|
||||
} else {
|
||||
return Err(crate::Error::SidecarNotAllowed(program));
|
||||
}
|
||||
} else {
|
||||
match shell.scope.prepare(&program, args) {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("{e}");
|
||||
return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)));
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(cwd) = options.cwd {
|
||||
command = command.current_dir(cwd);
|
||||
}
|
||||
if let Some(env) = options.env {
|
||||
command = command.envs(env);
|
||||
} else {
|
||||
command = command.env_clear();
|
||||
}
|
||||
let encoding = match options.encoding {
|
||||
Option::None => EncodingWrapper::Text(None),
|
||||
Some(encoding) => match encoding.as_str() {
|
||||
"raw" => EncodingWrapper::Raw,
|
||||
_ => {
|
||||
if let Some(text_encoding) = Encoding::for_label(encoding.as_bytes()) {
|
||||
EncodingWrapper::Text(Some(text_encoding))
|
||||
} else {
|
||||
return Err(crate::Error::UnknownEncoding(encoding));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let (mut rx, child) = command.spawn()?;
|
||||
|
||||
let pid = child.pid();
|
||||
shell.children.lock().unwrap().insert(pid, child);
|
||||
let children = shell.children.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if matches!(event, crate::process::CommandEvent::Terminated(_)) {
|
||||
children.lock().unwrap().remove(&pid);
|
||||
};
|
||||
let js_event = JSCommandEvent::new(event, encoding);
|
||||
let js = tauri::api::ipc::format_callback(on_event_fn, &js_event)
|
||||
.expect("unable to serialize CommandEvent");
|
||||
|
||||
let _ = window.eval(js.as_str());
|
||||
}
|
||||
});
|
||||
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stdin_write<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
pid: ChildId,
|
||||
buffer: Buffer,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(child) = shell.children.lock().unwrap().get_mut(&pid) {
|
||||
match buffer {
|
||||
Buffer::Text(t) => child.write(t.as_bytes())?,
|
||||
Buffer::Raw(r) => child.write(&r)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn kill<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
pid: ChildId,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(child) = shell.children.lock().unwrap().remove(&pid) {
|
||||
child.kill()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
path: String,
|
||||
with: Option<Program>,
|
||||
) -> crate::Result<()> {
|
||||
shell.open(path, with)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("current executable path has no parent")]
|
||||
CurrentExeHasNoParent,
|
||||
#[error("unknown program {0}")]
|
||||
UnknownProgramName(String),
|
||||
#[error(transparent)]
|
||||
Scope(#[from] crate::scope::Error),
|
||||
/// Sidecar not allowed by the configuration.
|
||||
#[error("sidecar not configured under `tauri.conf.json > tauri > bundle > externalBin`: {0}")]
|
||||
SidecarNotAllowed(PathBuf),
|
||||
/// Program not allowed by the scope.
|
||||
#[error("program not allowed on the configured shell scope: {0}")]
|
||||
ProgramNotAllowed(PathBuf),
|
||||
#[error("unknown encoding {0}")]
|
||||
UnknownEncoding(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use process::{Command, CommandChild};
|
||||
use regex::Regex;
|
||||
use scope::{Scope, ScopeAllowedCommand, ScopeConfig};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope},
|
||||
AppHandle, Manager, RunEvent, Runtime,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod open;
|
||||
pub mod process;
|
||||
mod scope;
|
||||
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
|
||||
|
||||
pub struct Shell<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
app: AppHandle<R>,
|
||||
scope: Scope,
|
||||
children: ChildStore,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Shell<R> {
|
||||
/// Creates a new Command for launching the given program.
|
||||
pub fn command(&self, program: impl Into<String>) -> 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 Into<String>) -> Result<Command> {
|
||||
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: String, with: Option<open::Program>) -> Result<()> {
|
||||
open::open(&self.scope, path, with).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> {
|
||||
Builder::new("shell")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::execute,
|
||||
commands::stdin_write,
|
||||
commands::kill,
|
||||
commands::open
|
||||
])
|
||||
.setup(|app, _api| {
|
||||
app.manage(Shell {
|
||||
app: app.clone(),
|
||||
children: Default::default(),
|
||||
scope: Scope::new(
|
||||
app,
|
||||
shell_scope(
|
||||
app.config().tauri.allowlist.shell.scope.clone(),
|
||||
&app.config().tauri.allowlist.shell.open,
|
||||
),
|
||||
),
|
||||
});
|
||||
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 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)
|
||||
}
|
||||
_ => panic!("unknown shell open format, unable to prepare"),
|
||||
};
|
||||
|
||||
ScopeConfig {
|
||||
open: shell_scope_open,
|
||||
scopes: shell_scopes,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap<String, ScopeAllowedCommand> {
|
||||
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 }
|
||||
}
|
||||
_ => panic!("unknown shell scope arg, unable to prepare"),
|
||||
});
|
||||
Some(list.collect())
|
||||
}
|
||||
_ => panic!("unknown shell scope command, unable to prepare"),
|
||||
};
|
||||
|
||||
(
|
||||
scope.name,
|
||||
ScopeAllowedCommand {
|
||||
command: scope.command,
|
||||
args,
|
||||
sidecar: scope.sidecar,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Types and functions related to shell.
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::scope::Scope;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Program to use on the [`open()`] call.
|
||||
pub enum Program {
|
||||
/// Use the `open` program.
|
||||
Open,
|
||||
/// Use the `start` program.
|
||||
Start,
|
||||
/// Use the `xdg-open` program.
|
||||
XdgOpen,
|
||||
/// Use the `gio` program.
|
||||
Gio,
|
||||
/// Use the `gnome-open` program.
|
||||
GnomeOpen,
|
||||
/// Use the `kde-open` program.
|
||||
KdeOpen,
|
||||
/// Use the `wslview` program.
|
||||
WslView,
|
||||
/// Use the `Firefox` program.
|
||||
Firefox,
|
||||
/// Use the `Google Chrome` program.
|
||||
Chrome,
|
||||
/// Use the `Chromium` program.
|
||||
Chromium,
|
||||
/// Use the `Safari` program.
|
||||
Safari,
|
||||
}
|
||||
|
||||
impl FromStr for Program {
|
||||
type Err = super::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let p = match s.to_lowercase().as_str() {
|
||||
"open" => Self::Open,
|
||||
"start" => Self::Start,
|
||||
"xdg-open" => Self::XdgOpen,
|
||||
"gio" => Self::Gio,
|
||||
"gnome-open" => Self::GnomeOpen,
|
||||
"kde-open" => Self::KdeOpen,
|
||||
"wslview" => Self::WslView,
|
||||
"firefox" => Self::Firefox,
|
||||
"chrome" | "google chrome" => Self::Chrome,
|
||||
"chromium" => Self::Chromium,
|
||||
"safari" => Self::Safari,
|
||||
_ => return Err(crate::Error::UnknownProgramName(s.to_string())),
|
||||
};
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Program {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Program::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "open",
|
||||
Self::Start => "start",
|
||||
Self::XdgOpen => "xdg-open",
|
||||
Self::Gio => "gio",
|
||||
Self::GnomeOpen => "gnome-open",
|
||||
Self::KdeOpen => "kde-open",
|
||||
Self::WslView => "wslview",
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::Firefox => "Firefox",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::Firefox => "firefox",
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::Chrome => "Google Chrome",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::Chrome => "google-chrome",
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::Chromium => "Chromium",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::Chromium => "chromium",
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::Safari => "Safari",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::Safari => "safari",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens path or URL with the program specified in `with`, or system default if `None`.
|
||||
///
|
||||
/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
/// A custom validation regex may be supplied in the config in `tauri > allowlist > scope > open`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri_plugin_shell::ShellExt;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// // open the given URL on the system default browser
|
||||
/// app.shell().open("https://github.com/tauri-apps/tauri", None)?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn open<P: AsRef<str>>(scope: &Scope, path: P, with: Option<Program>) -> crate::Result<()> {
|
||||
scope.open(path.as_ref(), with).map_err(Into::into)
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{BufReader, Write},
|
||||
path::PathBuf,
|
||||
process::{Command as StdCommand, Stdio},
|
||||
sync::{Arc, RwLock},
|
||||
thread::spawn,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
const NEWLINE_BYTE: u8 = b'\n';
|
||||
|
||||
use tauri::async_runtime::{block_on as block_on_task, channel, Receiver, Sender};
|
||||
|
||||
pub use encoding_rs::Encoding;
|
||||
use os_pipe::{pipe, PipeReader, PipeWriter};
|
||||
use serde::Serialize;
|
||||
use shared_child::SharedChild;
|
||||
use tauri::utils::platform;
|
||||
|
||||
/// Payload for the [`CommandEvent::Terminated`] command event.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TerminatedPayload {
|
||||
/// Exit code of the process.
|
||||
pub code: Option<i32>,
|
||||
/// If the process was terminated by a signal, represents that signal.
|
||||
pub signal: Option<i32>,
|
||||
}
|
||||
|
||||
/// A event sent to the command callback.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum CommandEvent {
|
||||
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
|
||||
Stderr(Vec<u8>),
|
||||
/// Stdout bytes until a newline (\n) or carriage return (\r) is found.
|
||||
Stdout(Vec<u8>),
|
||||
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string.
|
||||
Error(String),
|
||||
/// Command process terminated.
|
||||
Terminated(TerminatedPayload),
|
||||
}
|
||||
|
||||
/// The type to spawn commands.
|
||||
#[derive(Debug)]
|
||||
pub struct Command {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
env_clear: bool,
|
||||
env: HashMap<String, String>,
|
||||
current_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Spawned child process.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandChild {
|
||||
inner: Arc<SharedChild>,
|
||||
stdin_writer: PipeWriter,
|
||||
}
|
||||
|
||||
impl CommandChild {
|
||||
/// Writes to process stdin.
|
||||
pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
|
||||
self.stdin_writer.write_all(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a kill signal to the child.
|
||||
pub fn kill(self) -> crate::Result<()> {
|
||||
self.inner.kill()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the process pid.
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.inner.id()
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the result of a process after it has terminated.
|
||||
#[derive(Debug)]
|
||||
pub struct ExitStatus {
|
||||
code: Option<i32>,
|
||||
}
|
||||
|
||||
impl ExitStatus {
|
||||
/// Returns the exit code of the process, if any.
|
||||
pub fn code(&self) -> Option<i32> {
|
||||
self.code
|
||||
}
|
||||
|
||||
/// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status.
|
||||
pub fn success(&self) -> bool {
|
||||
self.code == Some(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The output of a finished process.
|
||||
#[derive(Debug)]
|
||||
pub struct Output {
|
||||
/// The status (exit code) of the process.
|
||||
pub status: ExitStatus,
|
||||
/// The data that the process wrote to stdout.
|
||||
pub stdout: Vec<u8>,
|
||||
/// The data that the process wrote to stderr.
|
||||
pub stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
fn relative_command_path(command: String) -> crate::Result<String> {
|
||||
match platform::current_exe()?.parent() {
|
||||
#[cfg(windows)]
|
||||
Some(exe_dir) => Ok(format!("{}\\{command}.exe", exe_dir.display())),
|
||||
#[cfg(not(windows))]
|
||||
Some(exe_dir) => Ok(format!("{}/{command}", exe_dir.display())),
|
||||
None => Err(crate::Error::CurrentExeHasNoParent),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Command> for StdCommand {
|
||||
fn from(cmd: Command) -> StdCommand {
|
||||
let mut command = StdCommand::new(cmd.program);
|
||||
command.args(cmd.args);
|
||||
command.stdout(Stdio::piped());
|
||||
command.stdin(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
if cmd.env_clear {
|
||||
command.env_clear();
|
||||
}
|
||||
command.envs(cmd.env);
|
||||
if let Some(current_dir) = cmd.current_dir {
|
||||
command.current_dir(current_dir);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(crate) fn new<S: Into<String>>(program: S) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
args: Default::default(),
|
||||
env_clear: false,
|
||||
env: Default::default(),
|
||||
current_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_sidecar<S: Into<String>>(program: S) -> crate::Result<Self> {
|
||||
Ok(Self::new(relative_command_path(program.into())?))
|
||||
}
|
||||
|
||||
/// Appends arguments to the command.
|
||||
#[must_use]
|
||||
pub fn args<I, S>(mut self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
for arg in args {
|
||||
self.args.push(arg.as_ref().to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Clears the entire environment map for the child process.
|
||||
#[must_use]
|
||||
pub fn env_clear(mut self) -> Self {
|
||||
self.env_clear = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds or updates multiple environment variable mappings.
|
||||
#[must_use]
|
||||
pub fn envs(mut self, env: HashMap<String, String>) -> Self {
|
||||
self.env = env;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the working directory for the child process.
|
||||
#[must_use]
|
||||
pub fn current_dir(mut self, current_dir: PathBuf) -> Self {
|
||||
self.current_dir.replace(current_dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Spawns the command.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::process::{Command, CommandEvent};
|
||||
/// tauri::async_runtime::spawn(async move {
|
||||
/// let (mut rx, mut child) = Command::new("cargo")
|
||||
/// .args(["tauri", "dev"])
|
||||
/// .spawn()
|
||||
/// .expect("Failed to spawn cargo");
|
||||
///
|
||||
/// let mut i = 0;
|
||||
/// while let Some(event) = rx.recv().await {
|
||||
/// if let CommandEvent::Stdout(line) = event {
|
||||
/// println!("got: {}", String::from_utf8(line).unwrap());
|
||||
/// i += 1;
|
||||
/// if i == 4 {
|
||||
/// child.write("message from Rust\n".as_bytes()).unwrap();
|
||||
/// i = 0;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
|
||||
let mut command: StdCommand = self.into();
|
||||
let (stdout_reader, stdout_writer) = pipe()?;
|
||||
let (stderr_reader, stderr_writer) = pipe()?;
|
||||
let (stdin_reader, stdin_writer) = pipe()?;
|
||||
command.stdout(stdout_writer);
|
||||
command.stderr(stderr_writer);
|
||||
command.stdin(stdin_reader);
|
||||
|
||||
let shared_child = SharedChild::spawn(&mut command)?;
|
||||
let child = Arc::new(shared_child);
|
||||
let child_ = child.clone();
|
||||
let guard = Arc::new(RwLock::new(()));
|
||||
|
||||
//TODO commands().lock().unwrap().insert(child.id(), child.clone());
|
||||
|
||||
let (tx, rx) = channel(1);
|
||||
|
||||
spawn_pipe_reader(
|
||||
tx.clone(),
|
||||
guard.clone(),
|
||||
stdout_reader,
|
||||
CommandEvent::Stdout,
|
||||
);
|
||||
spawn_pipe_reader(
|
||||
tx.clone(),
|
||||
guard.clone(),
|
||||
stderr_reader,
|
||||
CommandEvent::Stderr,
|
||||
);
|
||||
|
||||
spawn(move || {
|
||||
let _ = match child_.wait() {
|
||||
Ok(status) => {
|
||||
let _l = guard.write().unwrap();
|
||||
//TODO commands().lock().unwrap().remove(&child_.id());
|
||||
block_on_task(async move {
|
||||
tx.send(CommandEvent::Terminated(TerminatedPayload {
|
||||
code: status.code(),
|
||||
#[cfg(windows)]
|
||||
signal: None,
|
||||
#[cfg(unix)]
|
||||
signal: status.signal(),
|
||||
}))
|
||||
.await
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let _l = guard.write().unwrap();
|
||||
block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Ok((
|
||||
rx,
|
||||
CommandChild {
|
||||
inner: child,
|
||||
stdin_writer,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Executes a command as a child process, waiting for it to finish and collecting its exit status.
|
||||
/// Stdin, stdout and stderr are ignored.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::process::Command;
|
||||
/// let status = Command::new("which").args(["ls"]).status().unwrap();
|
||||
/// println!("`which` finished with status: {:?}", status.code());
|
||||
/// ```
|
||||
pub async fn status(self) -> crate::Result<ExitStatus> {
|
||||
let (mut rx, _child) = self.spawn()?;
|
||||
let mut code = None;
|
||||
#[allow(clippy::collapsible_match)]
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let CommandEvent::Terminated(payload) = event {
|
||||
code = payload.code;
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus { code })
|
||||
}
|
||||
|
||||
/// Executes the command as a child process, waiting for it to finish and collecting all of its output.
|
||||
/// Stdin is ignored.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::process::Command;
|
||||
/// let output = Command::new("echo").args(["TAURI"]).output().unwrap();
|
||||
/// assert!(output.status.success());
|
||||
/// assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI");
|
||||
/// ```
|
||||
pub async fn output(self) -> crate::Result<Output> {
|
||||
let (mut rx, _child) = self.spawn()?;
|
||||
|
||||
let mut code = None;
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => {
|
||||
code = payload.code;
|
||||
}
|
||||
CommandEvent::Stdout(line) => {
|
||||
stdout.extend(line);
|
||||
stdout.push(NEWLINE_BYTE);
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
stderr.extend(line);
|
||||
stderr.push(NEWLINE_BYTE);
|
||||
}
|
||||
CommandEvent::Error(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(Output {
|
||||
status: ExitStatus { code },
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
|
||||
tx: Sender<CommandEvent>,
|
||||
guard: Arc<RwLock<()>>,
|
||||
pipe_reader: PipeReader,
|
||||
wrapper: F,
|
||||
) {
|
||||
spawn(move || {
|
||||
let _lock = guard.read().unwrap();
|
||||
let mut reader = BufReader::new(pipe_reader);
|
||||
|
||||
loop {
|
||||
let mut buf = Vec::new();
|
||||
match tauri::utils::io::read_line(&mut reader, &mut buf) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let tx_ = tx.clone();
|
||||
let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
|
||||
}
|
||||
Err(e) => {
|
||||
let tx_ = tx.clone();
|
||||
let _ =
|
||||
block_on_task(
|
||||
async move { tx_.send(CommandEvent::Error(e.to_string())).await },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// tests for the commands functions.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(not(windows))]
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_spawn_output() {
|
||||
let cmd = Command::new("cat").args(["test/api/test.txt"]);
|
||||
let (mut rx, _) = cmd.spawn().unwrap();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => {
|
||||
assert_eq!(payload.code, Some(0));
|
||||
}
|
||||
CommandEvent::Stdout(line) => {
|
||||
assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_spawn_raw_output() {
|
||||
let cmd = Command::new("cat").args(["test/api/test.txt"]);
|
||||
let (mut rx, _) = cmd.spawn().unwrap();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => {
|
||||
assert_eq!(payload.code, Some(0));
|
||||
}
|
||||
CommandEvent::Stdout(line) => {
|
||||
assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
// test the failure case
|
||||
fn test_cmd_spawn_fail() {
|
||||
let cmd = Command::new("cat").args(["test/api/"]);
|
||||
let (mut rx, _) = cmd.spawn().unwrap();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => {
|
||||
assert_eq!(payload.code, Some(1));
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
assert_eq!(
|
||||
String::from_utf8(line).unwrap(),
|
||||
"cat: test/api/: Is a directory"
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
// test the failure case (raw encoding)
|
||||
fn test_cmd_spawn_raw_fail() {
|
||||
let cmd = Command::new("cat").args(["test/api/"]);
|
||||
let (mut rx, _) = cmd.spawn().unwrap();
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Terminated(payload) => {
|
||||
assert_eq!(payload.code, Some(1));
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
assert_eq!(
|
||||
String::from_utf8(line).unwrap(),
|
||||
"cat: test/api/: Is a directory"
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_output_output() {
|
||||
let cmd = Command::new("cat").args(["test/api/test.txt"]);
|
||||
let output = cmd.output().unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stdout).unwrap(),
|
||||
"This is a test doc!\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_output_output_fail() {
|
||||
let cmd = Command::new("cat").args(["test/api/"]);
|
||||
let output = cmd.output().unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stderr).unwrap(),
|
||||
"cat: test/api/: Is a directory\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::open::Program;
|
||||
use crate::process::Command;
|
||||
use crate::{Manager, Runtime};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Allowed representation of `Execute` command arguments.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ExecuteArgs {
|
||||
/// No arguments
|
||||
None,
|
||||
|
||||
/// A single string argument
|
||||
Single(String),
|
||||
|
||||
/// Multiple string arguments
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
impl ExecuteArgs {
|
||||
/// Whether the argument list is empty or not.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::None => true,
|
||||
Self::Single(s) if s.is_empty() => true,
|
||||
Self::List(l) => l.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for ExecuteArgs {
|
||||
fn from(_: ()) -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ExecuteArgs {
|
||||
fn from(string: String) -> Self {
|
||||
Self::Single(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for ExecuteArgs {
|
||||
fn from(vec: Vec<String>) -> Self {
|
||||
Self::List(vec)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell scope configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeConfig {
|
||||
/// The validation regex that `shell > open` paths must match against.
|
||||
pub open: Option<Regex>,
|
||||
|
||||
/// All allowed commands, using their unique command name as the keys.
|
||||
pub scopes: HashMap<String, ScopeAllowedCommand>,
|
||||
}
|
||||
|
||||
/// A configured scoped shell command.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeAllowedCommand {
|
||||
/// The shell command to be called.
|
||||
pub command: std::path::PathBuf,
|
||||
|
||||
/// The arguments the command is allowed to be called with.
|
||||
pub args: Option<Vec<ScopeAllowedArg>>,
|
||||
|
||||
/// If this command is a sidecar command.
|
||||
pub sidecar: bool,
|
||||
}
|
||||
|
||||
/// A configured argument to a scoped shell command.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ScopeAllowedArg {
|
||||
/// A non-configurable argument.
|
||||
Fixed(String),
|
||||
|
||||
/// An argument with a value to be evaluated at runtime, must pass a regex validation.
|
||||
Var {
|
||||
/// The validation that the variable value must pass in order to be called.
|
||||
validator: Regex,
|
||||
},
|
||||
}
|
||||
|
||||
impl ScopeAllowedArg {
|
||||
/// If the argument is fixed.
|
||||
pub fn is_fixed(&self) -> bool {
|
||||
matches!(self, Self::Fixed(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Scope for filesystem access.
|
||||
#[derive(Clone)]
|
||||
pub struct Scope(ScopeConfig);
|
||||
|
||||
/// All errors that can happen while validating a scoped command.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// At least one argument did not pass input validation.
|
||||
#[error("The scoped command was called with the improper sidecar flag set")]
|
||||
BadSidecarFlag,
|
||||
|
||||
/// The sidecar program validated but failed to find the sidecar path.
|
||||
#[error(
|
||||
"The scoped sidecar command was validated, but failed to create the path to the command: {0}"
|
||||
)]
|
||||
Sidecar(String),
|
||||
|
||||
/// The named command was not found in the scoped config.
|
||||
#[error("Scoped command {0} not found")]
|
||||
NotFound(String),
|
||||
|
||||
/// A command variable has no value set in the arguments.
|
||||
#[error(
|
||||
"Scoped command argument at position {0} must match regex validation {1} but it was not found"
|
||||
)]
|
||||
MissingVar(usize, String),
|
||||
|
||||
/// At least one argument did not pass input validation.
|
||||
#[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")]
|
||||
Validation {
|
||||
/// Index of the variable.
|
||||
index: usize,
|
||||
|
||||
/// Regex that the variable value failed to match.
|
||||
validation: String,
|
||||
},
|
||||
|
||||
/// The format of the passed input does not match the expected shape.
|
||||
///
|
||||
/// This can happen from passing a string or array of strings to a command that is expecting
|
||||
/// named variables, and vice-versa.
|
||||
#[error("Scoped command {0} received arguments in an unexpected format")]
|
||||
InvalidInput(String),
|
||||
|
||||
/// A generic IO error that occurs while executing specified shell commands.
|
||||
#[error("Scoped shell IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Creates a new shell scope.
|
||||
pub(crate) fn new<R: Runtime, M: Manager<R>>(manager: &M, mut scope: ScopeConfig) -> Self {
|
||||
for cmd in scope.scopes.values_mut() {
|
||||
if let Ok(path) = manager.path().parse(&cmd.command) {
|
||||
cmd.command = path;
|
||||
}
|
||||
}
|
||||
Self(scope)
|
||||
}
|
||||
|
||||
/// Validates argument inputs and creates a Tauri sidecar [`Command`].
|
||||
pub fn prepare_sidecar(
|
||||
&self,
|
||||
command_name: &str,
|
||||
command_script: &str,
|
||||
args: ExecuteArgs,
|
||||
) -> Result<Command, Error> {
|
||||
self._prepare(command_name, args, Some(command_script))
|
||||
}
|
||||
|
||||
/// Validates argument inputs and creates a Tauri [`Command`].
|
||||
pub fn prepare(&self, command_name: &str, args: ExecuteArgs) -> Result<Command, Error> {
|
||||
self._prepare(command_name, args, None)
|
||||
}
|
||||
|
||||
/// Validates argument inputs and creates a Tauri [`Command`].
|
||||
pub fn _prepare(
|
||||
&self,
|
||||
command_name: &str,
|
||||
args: ExecuteArgs,
|
||||
sidecar: Option<&str>,
|
||||
) -> Result<Command, Error> {
|
||||
let command = match self.0.scopes.get(command_name) {
|
||||
Some(command) => command,
|
||||
None => return Err(Error::NotFound(command_name.into())),
|
||||
};
|
||||
|
||||
if command.sidecar != sidecar.is_some() {
|
||||
return Err(Error::BadSidecarFlag);
|
||||
}
|
||||
|
||||
let args = match (&command.args, args) {
|
||||
(None, ExecuteArgs::None) => Ok(vec![]),
|
||||
(None, ExecuteArgs::List(list)) => Ok(list),
|
||||
(None, ExecuteArgs::Single(string)) => Ok(vec![string]),
|
||||
(Some(list), ExecuteArgs::List(args)) => list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
|
||||
ScopeAllowedArg::Var { validator } => {
|
||||
let value = args
|
||||
.get(i)
|
||||
.ok_or_else(|| Error::MissingVar(i, validator.to_string()))?
|
||||
.to_string();
|
||||
if validator.is_match(&value) {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(Error::Validation {
|
||||
index: i,
|
||||
validation: validator.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
(Some(list), arg) if arg.is_empty() && list.iter().all(ScopeAllowedArg::is_fixed) => {
|
||||
list.iter()
|
||||
.map(|arg| match arg {
|
||||
ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
(Some(list), _) if list.is_empty() => Err(Error::InvalidInput(command_name.into())),
|
||||
(Some(_), _) => Err(Error::InvalidInput(command_name.into())),
|
||||
}?;
|
||||
|
||||
let command_s = sidecar
|
||||
.map(|s| {
|
||||
std::path::PathBuf::from(s)
|
||||
.components()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_os_str()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.unwrap_or_else(|| command.command.to_string_lossy().into_owned());
|
||||
let command = if command.sidecar {
|
||||
Command::new_sidecar(command_s).map_err(|e| Error::Sidecar(e.to_string()))?
|
||||
} else {
|
||||
Command::new(command_s)
|
||||
};
|
||||
|
||||
Ok(command.args(args))
|
||||
}
|
||||
|
||||
/// Open a path in the default (or specified) browser.
|
||||
///
|
||||
/// The path is validated against the `tauri > allowlist > shell > open` validation regex, which
|
||||
/// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
|
||||
pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), Error> {
|
||||
// ensure we pass validation if the configuration has one
|
||||
if let Some(regex) = &self.0.open {
|
||||
if !regex.is_match(path) {
|
||||
return Err(Error::Validation {
|
||||
index: 0,
|
||||
validation: regex.as_str().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by
|
||||
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
|
||||
match with.map(Program::name) {
|
||||
Some(program) => ::open::with(path, program),
|
||||
None => ::open::that(path),
|
||||
}
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user