mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-05-29 13:31:27 +02:00
Merge branch 'v2' into feat/shell-show-item-in-dir
This commit is contained in:
@@ -1 +0,0 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_SHELL__=function(e){"use strict";function t(e,t,s,r){if("a"===s&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?r:"a"===s?r.call(e):r?r.value:t.get(e)}var s;"function"==typeof SuppressedError&&SuppressedError;class r{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,s.set(this,(()=>{})),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((e=>{t(this,s,"f").call(this,e)}))}set onmessage(e){!function(e,t,s,r,n){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!n)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===r?n.call(e,s):n?n.value=s:t.set(e,s)}(this,s,e,"f")}get onmessage(){return t(this,s,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function n(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}s=new WeakMap;class i{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){const s=r=>{this.removeListener(e,s),t(r)};return this.addListener(e,s)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter((e=>e!==t))),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,t){if(e in this.eventListeners){const s=this.eventListeners[e];for(const e of s)e(t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){const s=r=>{this.removeListener(e,s),t(r)};return this.prependListener(e,s)}}class o{constructor(e){this.pid=e}async write(e){return n("plugin:shell|stdin_write",{pid:this.pid,buffer:"string"==typeof e?e:Array.from(e)})}async kill(){return n("plugin:shell|kill",{cmd:"killChild",pid:this.pid})}}class a extends i{constructor(e,t=[],s){super(),this.stdout=new i,this.stderr=new i,this.program=e,this.args="string"==typeof t?[t]:t,this.options=s??{}}static create(e,t=[],s){return new a(e,t,s)}static sidecar(e,t=[],s){const r=new a(e,t,s);return r.options.sidecar=!0,r}async spawn(){return async function(e,t,s=[],i){"object"==typeof s&&Object.freeze(s);const o=new r;return o.onmessage=e,n("plugin:shell|execute",{program:t,args:s,options:i,onEvent:o})}((e=>{switch(e.event){case"Error":this.emit("error",e.payload);break;case"Terminated":this.emit("close",e.payload);break;case"Stdout":this.stdout.emit("data",e.payload);break;case"Stderr":this.stderr.emit("data",e.payload)}}),this.program,this.args,this.options).then((e=>new o(e)))}async execute(){return new Promise(((e,t)=>{this.on("error",t);const s=[],r=[];this.stdout.on("data",(e=>{s.push(e)})),this.stderr.on("data",(e=>{r.push(e)})),this.on("close",(t=>{e({code:t.code,signal:t.signal,stdout:this.collectOutput(s),stderr:this.collectOutput(r)})})),this.spawn().catch(t)}))}collectOutput(e){return"raw"===this.options.encoding?e.reduce(((e,t)=>new Uint8Array([...e,...t,10])),new Uint8Array):e.join("\n")}}return e.Child=o,e.Command=a,e.EventEmitter=i,e.open=async function(e,t){return n("plugin:shell|open",{path:e,with:t})},e}({});Object.defineProperty(window.__TAURI__,"shell",{value:__TAURI_PLUGIN_SHELL__})}
|
||||
+101
-11
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
|
||||
use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, string::FromUtf8Error};
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -23,7 +23,7 @@ type ChildId = u32;
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "event", content = "payload")]
|
||||
#[non_exhaustive]
|
||||
enum JSCommandEvent {
|
||||
pub 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.
|
||||
@@ -94,18 +94,15 @@ fn default_env() -> Option<HashMap<String, String>> {
|
||||
Some(HashMap::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub fn execute<R: Runtime>(
|
||||
#[inline(always)]
|
||||
fn prepare_cmd<R: Runtime>(
|
||||
window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
program: String,
|
||||
args: ExecuteArgs,
|
||||
on_event: Channel,
|
||||
options: CommandOptions,
|
||||
command_scope: CommandScope<crate::scope::ScopeAllowedCommand>,
|
||||
global_scope: GlobalScope<crate::scope::ScopeAllowedCommand>,
|
||||
) -> crate::Result<ChildId> {
|
||||
) -> crate::Result<(crate::process::Command, EncodingWrapper)> {
|
||||
let scope = crate::scope::ShellScope {
|
||||
scopes: command_scope
|
||||
.allows()
|
||||
@@ -151,10 +148,14 @@ pub fn execute<R: Runtime>(
|
||||
} else {
|
||||
command = command.env_clear();
|
||||
}
|
||||
|
||||
let encoding = match options.encoding {
|
||||
Option::None => EncodingWrapper::Text(None),
|
||||
Some(encoding) => match encoding.as_str() {
|
||||
"raw" => EncodingWrapper::Raw,
|
||||
"raw" => {
|
||||
command = command.set_raw_out(true);
|
||||
EncodingWrapper::Raw
|
||||
}
|
||||
_ => {
|
||||
if let Some(text_encoding) = Encoding::for_label(encoding.as_bytes()) {
|
||||
EncodingWrapper::Text(Some(text_encoding))
|
||||
@@ -165,6 +166,81 @@ pub fn execute<R: Runtime>(
|
||||
},
|
||||
};
|
||||
|
||||
Ok((command, encoding))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum Output {
|
||||
String(String),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChildProcessReturn {
|
||||
code: Option<i32>,
|
||||
signal: Option<i32>,
|
||||
stdout: Output,
|
||||
stderr: Output,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub async fn execute<R: Runtime>(
|
||||
window: Window<R>,
|
||||
program: String,
|
||||
args: ExecuteArgs,
|
||||
options: CommandOptions,
|
||||
command_scope: CommandScope<crate::scope::ScopeAllowedCommand>,
|
||||
global_scope: GlobalScope<crate::scope::ScopeAllowedCommand>,
|
||||
) -> crate::Result<ChildProcessReturn> {
|
||||
let (command, encoding) =
|
||||
prepare_cmd(window, program, args, options, command_scope, global_scope)?;
|
||||
|
||||
let mut command: std::process::Command = command.into();
|
||||
let output = command.output()?;
|
||||
|
||||
let (stdout, stderr) = match encoding {
|
||||
EncodingWrapper::Text(Some(encoding)) => (
|
||||
Output::String(encoding.decode_with_bom_removal(&output.stdout).0.into()),
|
||||
Output::String(encoding.decode_with_bom_removal(&output.stderr).0.into()),
|
||||
),
|
||||
EncodingWrapper::Text(None) => (
|
||||
Output::String(String::from_utf8(output.stdout)?),
|
||||
Output::String(String::from_utf8(output.stderr)?),
|
||||
),
|
||||
EncodingWrapper::Raw => (Output::Raw(output.stdout), Output::Raw(output.stderr)),
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
Ok(ChildProcessReturn {
|
||||
code: output.status.code(),
|
||||
#[cfg(windows)]
|
||||
signal: None,
|
||||
#[cfg(unix)]
|
||||
signal: output.status.signal(),
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tauri::command]
|
||||
pub fn spawn<R: Runtime>(
|
||||
window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
program: String,
|
||||
args: ExecuteArgs,
|
||||
on_event: Channel<JSCommandEvent>,
|
||||
options: CommandOptions,
|
||||
command_scope: CommandScope<crate::scope::ScopeAllowedCommand>,
|
||||
global_scope: GlobalScope<crate::scope::ScopeAllowedCommand>,
|
||||
) -> crate::Result<ChildId> {
|
||||
let (command, encoding) =
|
||||
prepare_cmd(window, program, args, options, command_scope, global_scope)?;
|
||||
|
||||
let (mut rx, child) = command.spawn()?;
|
||||
|
||||
let pid = child.pid();
|
||||
@@ -177,7 +253,21 @@ pub fn execute<R: Runtime>(
|
||||
children.lock().unwrap().remove(&pid);
|
||||
};
|
||||
let js_event = JSCommandEvent::new(event, encoding);
|
||||
let _ = on_event.send(&js_event);
|
||||
|
||||
if on_event.send(js_event.clone()).is_err() {
|
||||
fn send<'a>(
|
||||
on_event: &'a Channel<JSCommandEvent>,
|
||||
js_event: &'a JSCommandEvent,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(15)).await;
|
||||
if on_event.send(js_event.clone()).is_err() {
|
||||
send(on_event, js_event).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
send(&on_event, &js_event).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -213,7 +303,7 @@ pub fn kill<R: Runtime>(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open<R: Runtime>(
|
||||
pub async fn open<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
shell: State<'_, Shell<R>>,
|
||||
path: String,
|
||||
|
||||
@@ -25,6 +25,9 @@ pub enum ShellAllowlistOpen {
|
||||
|
||||
/// Enable the shell open API, with a custom regex that the opened path must match against.
|
||||
///
|
||||
/// The regex string is automatically surrounded by `^...$` to match the full string.
|
||||
/// For example the `https?://\w+` regex would be registered as `^https?://\w+$`.
|
||||
///
|
||||
/// If using a custom regex to support a non-http(s) schema, care should be used to prevent values
|
||||
/// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.
|
||||
Validate(String),
|
||||
|
||||
@@ -8,6 +8,9 @@ use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("current executable path has no parent")]
|
||||
@@ -17,7 +20,7 @@ pub enum Error {
|
||||
#[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}")]
|
||||
#[error("sidecar not configured under `tauri.conf.json > bundle > externalBin`: {0}")]
|
||||
SidecarNotAllowed(PathBuf),
|
||||
/// Program not allowed by the scope.
|
||||
#[error("program not allowed on the configured shell scope: {0}")]
|
||||
@@ -36,6 +39,9 @@ pub enum Error {
|
||||
/// Path doesn't have a parent.
|
||||
#[error("Path doesn't have a parent: {0}")]
|
||||
NoParent(PathBuf),
|
||||
/// Utf8 error.
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
@@ -1 +1 @@
|
||||
!function(){"use strict";async function e(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function t(){document.querySelector("body")?.addEventListener("click",(function(t){let n=t.target;for(;null!=n;){if(n.matches("a")){const r=n;r.href&&["http://","https://","mailto:","tel:"].some((e=>r.href.startsWith(e)))&&"_blank"===r.target&&(e("plugin:shell|open",{path:r.href}),t.preventDefault());break}n=n.parentElement}}))}"function"==typeof SuppressedError&&SuppressedError,"complete"===document.readyState||"interactive"===document.readyState?t():window.addEventListener("DOMContentLoaded",t,!0)}();
|
||||
!function(){"use strict";async function e(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function t(){document.querySelector("body")?.addEventListener("click",(function(t){let n=t.target;for(;n;){if(n.matches("a")){const r=n;""!==r.href&&["http://","https://","mailto:","tel:"].some((e=>r.href.startsWith(e)))&&"_blank"===r.target&&(e("plugin:shell|open",{path:r.href}),t.preventDefault());break}n=n.parentElement}}))}"function"==typeof SuppressedError&&SuppressedError,"complete"===document.readyState||"interactive"===document.readyState?t():window.addEventListener("DOMContentLoaded",t,!0)}();
|
||||
|
||||
@@ -35,11 +35,21 @@ 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,
|
||||
}
|
||||
@@ -61,10 +71,21 @@ impl<R: Runtime> Shell<R> {
|
||||
/// 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)]
|
||||
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
|
||||
open::open(&self.open_scope, path.into(), with).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
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 fn show_item_in_directory<P: AsRef<Path>>(&self, p: P) -> Result<()> {
|
||||
open::show_item_in_directory(p)
|
||||
}
|
||||
@@ -81,13 +102,11 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
let mut init_script = include_str!("init-iife.js").to_string();
|
||||
init_script.push_str(include_str!("api-iife.js"));
|
||||
|
||||
Builder::<R, Option<config::Config>>::new("shell")
|
||||
.js_init_script(init_script)
|
||||
.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
|
||||
@@ -95,10 +114,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
.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(())
|
||||
})
|
||||
@@ -124,8 +152,9 @@ fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
|
||||
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
|
||||
}
|
||||
config::ShellAllowlistOpen::Validate(validator) => {
|
||||
let regex = format!("^{validator}$");
|
||||
let validator =
|
||||
Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
Regex::new(®ex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
|
||||
Some(validator)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
io::{BufReader, Write},
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command as StdCommand, Stdio},
|
||||
sync::{Arc, RwLock},
|
||||
@@ -41,11 +41,13 @@ pub struct TerminatedPayload {
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum CommandEvent {
|
||||
/// Stderr bytes until a newline (\n) or carriage return (\r) is found.
|
||||
/// If configured for raw output, all bytes written to stderr.
|
||||
/// Otherwise, 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.
|
||||
/// If configured for raw output, all bytes written to stdout.
|
||||
/// Otherwise, 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.
|
||||
/// An error happened waiting for the command to finish or converting the stdout/stderr bytes to a UTF-8 string.
|
||||
Error(String),
|
||||
/// Command process terminated.
|
||||
Terminated(TerminatedPayload),
|
||||
@@ -53,7 +55,10 @@ pub enum CommandEvent {
|
||||
|
||||
/// The type to spawn commands.
|
||||
#[derive(Debug)]
|
||||
pub struct Command(StdCommand);
|
||||
pub struct Command {
|
||||
cmd: StdCommand,
|
||||
raw_out: bool,
|
||||
}
|
||||
|
||||
/// Spawned child process.
|
||||
#[derive(Debug)]
|
||||
@@ -122,7 +127,7 @@ fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
|
||||
|
||||
impl From<Command> for StdCommand {
|
||||
fn from(cmd: Command) -> StdCommand {
|
||||
cmd.0
|
||||
cmd.cmd
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +141,10 @@ impl Command {
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
|
||||
Self(command)
|
||||
Self {
|
||||
cmd: command,
|
||||
raw_out: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
|
||||
@@ -146,7 +154,7 @@ impl Command {
|
||||
/// Appends an argument to the command.
|
||||
#[must_use]
|
||||
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
|
||||
self.0.arg(arg);
|
||||
self.cmd.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -157,14 +165,14 @@ impl Command {
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
self.0.args(args);
|
||||
self.cmd.args(args);
|
||||
self
|
||||
}
|
||||
|
||||
/// Clears the entire environment map for the child process.
|
||||
#[must_use]
|
||||
pub fn env_clear(mut self) -> Self {
|
||||
self.0.env_clear();
|
||||
self.cmd.env_clear();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -175,7 +183,7 @@ impl Command {
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>,
|
||||
{
|
||||
self.0.env(key, value);
|
||||
self.cmd.env(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -187,14 +195,20 @@ impl Command {
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>,
|
||||
{
|
||||
self.0.envs(envs);
|
||||
self.cmd.envs(envs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the working directory for the child process.
|
||||
#[must_use]
|
||||
pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
|
||||
self.0.current_dir(current_dir);
|
||||
self.cmd.current_dir(current_dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures the reader to output bytes from the child process exactly as received
|
||||
pub fn set_raw_out(mut self, raw_out: bool) -> Self {
|
||||
self.raw_out = raw_out;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -229,6 +243,7 @@ impl Command {
|
||||
/// });
|
||||
/// ```
|
||||
pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
|
||||
let raw = self.raw_out;
|
||||
let mut command: StdCommand = self.into();
|
||||
let (stdout_reader, stdout_writer) = pipe()?;
|
||||
let (stderr_reader, stderr_writer) = pipe()?;
|
||||
@@ -249,12 +264,14 @@ impl Command {
|
||||
guard.clone(),
|
||||
stdout_reader,
|
||||
CommandEvent::Stdout,
|
||||
raw,
|
||||
);
|
||||
spawn_pipe_reader(
|
||||
tx.clone(),
|
||||
guard.clone(),
|
||||
stderr_reader,
|
||||
CommandEvent::Stderr,
|
||||
raw,
|
||||
);
|
||||
|
||||
spawn(move || {
|
||||
@@ -359,35 +376,74 @@ impl Command {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
|
||||
mut reader: BufReader<PipeReader>,
|
||||
tx: Sender<CommandEvent>,
|
||||
wrapper: F,
|
||||
) {
|
||||
loop {
|
||||
let result = reader.fill_buf();
|
||||
match result {
|
||||
Ok(buf) => {
|
||||
let length = buf.len();
|
||||
if length == 0 {
|
||||
break;
|
||||
}
|
||||
let tx_ = tx.clone();
|
||||
let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
|
||||
reader.consume(length);
|
||||
}
|
||||
Err(e) => {
|
||||
let tx_ = tx.clone();
|
||||
let _ = block_on_task(
|
||||
async move { tx_.send(CommandEvent::Error(e.to_string())).await },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
|
||||
mut reader: BufReader<PipeReader>,
|
||||
tx: Sender<CommandEvent>,
|
||||
wrapper: F,
|
||||
) {
|
||||
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 },
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
|
||||
tx: Sender<CommandEvent>,
|
||||
guard: Arc<RwLock<()>>,
|
||||
pipe_reader: PipeReader,
|
||||
wrapper: F,
|
||||
raw_out: bool,
|
||||
) {
|
||||
spawn(move || {
|
||||
let _lock = guard.read().unwrap();
|
||||
let mut reader = BufReader::new(pipe_reader);
|
||||
let 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 },
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if raw_out {
|
||||
read_raw_bytes(reader, tx, wrapper);
|
||||
} else {
|
||||
read_line(reader, tx, wrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::open::Program;
|
||||
use crate::process::Command;
|
||||
|
||||
@@ -86,9 +88,14 @@ impl ScopeObject for ScopeAllowedCommand {
|
||||
crate::scope_entry::ShellAllowedArg::Fixed(fixed) => {
|
||||
crate::scope::ScopeAllowedArg::Fixed(fixed)
|
||||
}
|
||||
crate::scope_entry::ShellAllowedArg::Var { validator } => {
|
||||
let validator = Regex::new(&validator)
|
||||
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
crate::scope_entry::ShellAllowedArg::Var { validator, raw } => {
|
||||
let regex = if raw {
|
||||
validator
|
||||
} else {
|
||||
format!("^{validator}$")
|
||||
};
|
||||
let validator = Regex::new(®ex)
|
||||
.unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
|
||||
crate::scope::ScopeAllowedArg::Var { validator }
|
||||
}
|
||||
});
|
||||
@@ -141,7 +148,7 @@ pub struct OpenScope {
|
||||
#[derive(Clone)]
|
||||
pub struct ShellScope<'a> {
|
||||
/// All allowed commands, using their unique command name as the keys.
|
||||
pub scopes: Vec<&'a ScopeAllowedCommand>,
|
||||
pub scopes: Vec<&'a Arc<ScopeAllowedCommand>>,
|
||||
}
|
||||
|
||||
/// All errors that can happen while validating a scoped command.
|
||||
|
||||
@@ -7,28 +7,23 @@ use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A command allowed to be executed by the webview API.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, schemars::JsonSchema)]
|
||||
pub struct Entry {
|
||||
/// The name for this allowed shell command configuration.
|
||||
///
|
||||
/// This name will be used inside of the webview API to call this command along with
|
||||
/// any specified arguments.
|
||||
pub name: String,
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct Entry {
|
||||
pub(crate) name: String,
|
||||
pub(crate) command: PathBuf,
|
||||
pub(crate) args: ShellAllowedArgs,
|
||||
pub(crate) sidecar: bool,
|
||||
}
|
||||
|
||||
/// The command name.
|
||||
/// It can start with a variable that resolves to a system base directory.
|
||||
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
||||
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
||||
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||
// use default just so the schema doesn't flag it as required
|
||||
pub command: PathBuf,
|
||||
|
||||
/// The allowed arguments for the command execution.
|
||||
pub args: ShellAllowedArgs,
|
||||
|
||||
/// If this command is a sidecar command.
|
||||
pub sidecar: bool,
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct EntryRaw {
|
||||
pub(crate) name: String,
|
||||
#[serde(rename = "cmd")]
|
||||
pub(crate) command: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub(crate) args: ShellAllowedArgs,
|
||||
#[serde(default)]
|
||||
pub(crate) sidecar: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Entry {
|
||||
@@ -36,18 +31,7 @@ impl<'de> Deserialize<'de> for Entry {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct InnerEntry {
|
||||
name: String,
|
||||
#[serde(rename = "cmd")]
|
||||
command: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
args: ShellAllowedArgs,
|
||||
#[serde(default)]
|
||||
sidecar: bool,
|
||||
}
|
||||
|
||||
let config = InnerEntry::deserialize(deserializer)?;
|
||||
let config = EntryRaw::deserialize(deserializer)?;
|
||||
|
||||
if !config.sidecar && config.command.is_none() {
|
||||
return Err(DeError::custom(
|
||||
@@ -64,19 +48,11 @@ impl<'de> Deserialize<'de> for Entry {
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of command arguments allowed to be executed by the webview API.
|
||||
///
|
||||
/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
|
||||
/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
|
||||
/// be passed to the attached command configuration.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellAllowedArgs {
|
||||
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
|
||||
Flag(bool),
|
||||
|
||||
/// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
|
||||
List(Vec<ShellAllowedArg>),
|
||||
}
|
||||
|
||||
@@ -86,23 +62,14 @@ impl Default for ShellAllowedArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// A command argument allowed to be executed by the webview API.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, schemars::JsonSchema)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellAllowedArg {
|
||||
/// A non-configurable argument that is passed to the command in the order it was specified.
|
||||
Fixed(String),
|
||||
|
||||
/// A variable that is set while calling the command from the webview API.
|
||||
///
|
||||
Var {
|
||||
/// [regex] validator to require passed values to conform to an expected input.
|
||||
///
|
||||
/// This will require the argument value passed to this variable to match the `validator` regex
|
||||
/// before it will be executed.
|
||||
///
|
||||
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
|
||||
validator: String,
|
||||
#[serde(default)]
|
||||
raw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user