mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-06 13:53:54 +02:00
Merge branch 'v2' into feat/shell-show-item-in-dir
This commit is contained in:
@@ -1,5 +1,82 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.0.1]
|
||||
|
||||
- [`51ddf6a7`](https://github.com/tauri-apps/plugins-workspace/commit/51ddf6a71544acfb261ffc9393dab1342da0a219) ([#1881](https://github.com/tauri-apps/plugins-workspace/pull/1881) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) On Windows, Fix `open` JS API hanging and freezing the app.
|
||||
|
||||
## \[2.0.1]
|
||||
|
||||
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
|
||||
|
||||
## \[2.0.0]
|
||||
|
||||
- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release.
|
||||
|
||||
## \[2.0.0-rc.4]
|
||||
|
||||
- [`44273b98`](https://github.com/tauri-apps/plugins-workspace/commit/44273b988957a254eff715d6be7547d2ace882e1) ([#1839](https://github.com/tauri-apps/plugins-workspace/pull/1839) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix the plugin schema requiring to set `sidecar` property when it is in fact optional.
|
||||
|
||||
## \[2.0.0-rc.1]
|
||||
|
||||
- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8
|
||||
|
||||
## \[2.0.0-rc.2]
|
||||
|
||||
- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
|
||||
|
||||
## \[2.0.0-rc.1]
|
||||
|
||||
### changes
|
||||
|
||||
- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0
|
||||
|
||||
## \[2.0.0-rc.0]
|
||||
|
||||
- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC.
|
||||
- [`34df132f`](https://github.com/tauri-apps/plugins-workspace/commit/34df132fb14212ba7330adc9ccd64267751950c8) ([#1603](https://github.com/tauri-apps/plugins-workspace/pull/1603)) Change the `open` scope validator regex to match on the entire string.
|
||||
- [`34df132f`](https://github.com/tauri-apps/plugins-workspace/commit/34df132fb14212ba7330adc9ccd64267751950c8) ([#1603](https://github.com/tauri-apps/plugins-workspace/pull/1603)) Change the `execute` scope argument validator regex to match on the entire string by default.
|
||||
If this behavior is not desired check the `raw` boolean configuration option that is available along the `validator` string.
|
||||
|
||||
## \[2.0.0-beta.9]
|
||||
|
||||
- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version.
|
||||
- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25.
|
||||
|
||||
## \[2.0.0-beta.8]
|
||||
|
||||
- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24.
|
||||
|
||||
## \[2.0.0-beta.7]
|
||||
|
||||
- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23.
|
||||
|
||||
## \[2.0.0-beta.6]
|
||||
|
||||
- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22.
|
||||
|
||||
## \[2.0.0-beta.5]
|
||||
|
||||
- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20.
|
||||
|
||||
## \[2.0.0-beta.4]
|
||||
|
||||
- [`eb1679b9`](https://github.com/tauri-apps/plugins-workspace/commit/eb1679b99780e5d2b867f5649a1ccc2f3f70ab56)([#1299](https://github.com/tauri-apps/plugins-workspace/pull/1299)) Fix `Command.execute` API including extra new lines.
|
||||
- [`eb1679b9`](https://github.com/tauri-apps/plugins-workspace/commit/eb1679b99780e5d2b867f5649a1ccc2f3f70ab56)([#1299](https://github.com/tauri-apps/plugins-workspace/pull/1299)) Improve the speed of the JS `Command.execute` API
|
||||
|
||||
## \[2.0.0-beta.3]
|
||||
|
||||
- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17.
|
||||
|
||||
## \[2.0.0-beta.3]
|
||||
|
||||
- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true.
|
||||
- [`040004a`](https://github.com/tauri-apps/plugins-workspace/commit/040004a6b9fbb89161d1b5764d79428dfe693776)([#1069](https://github.com/tauri-apps/plugins-workspace/pull/1069)) Change shell's schema property name `command` to `cmd`.
|
||||
|
||||
## \[2.0.0-beta.2]
|
||||
|
||||
- [`9586eab`](https://github.com/tauri-apps/plugins-workspace/commit/9586eabd5a96673e4d976757777f470ae358d68a)([#1021](https://github.com/tauri-apps/plugins-workspace/pull/1021)) On Windows, fix `open` can't open file if the file is being used by a program.
|
||||
- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8.
|
||||
|
||||
## \[2.0.0-beta.1]
|
||||
|
||||
- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4.
|
||||
@@ -49,4 +126,21 @@
|
||||
|
||||
- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
rkspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
!
|
||||
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
[package]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.0.0-beta.1"
|
||||
version = "2.0.2"
|
||||
description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
links = "tauri-plugin-shell"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.platforms.support]
|
||||
windows = { level = "full", notes = "" }
|
||||
linux = { level = "full", notes = "" }
|
||||
macos = { level = "full", notes = "" }
|
||||
android = { level = "partial", notes = "Only allows to open URLs via `open`" }
|
||||
ios = { level = "partial", notes = "Only allows to open URLs via `open`" }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { workspace = true, features = ["build"] }
|
||||
schemars = { workspace = true }
|
||||
@@ -19,14 +27,14 @@ serde = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
shared_child = "1"
|
||||
regex = "1"
|
||||
open = "4"
|
||||
open = { version = "5", features = ["shellexecute-on-windows"] }
|
||||
encoding_rs = "0.8"
|
||||
os_pipe = "1"
|
||||
|
||||
@@ -38,3 +46,6 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Com",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
tauri = { workspace = true, features = ["wry"] }
|
||||
|
||||
+12
-4
@@ -2,9 +2,17 @@
|
||||
|
||||
Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application.
|
||||
|
||||
| Platform | Supported |
|
||||
| -------- | --------- |
|
||||
| Linux | ✓ |
|
||||
| Windows | ✓ |
|
||||
| macOS | ✓ |
|
||||
| Android | ✓ |
|
||||
| iOS | ✓ |
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.75**_
|
||||
_This plugin requires a Rust version of at least **1.77.2**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
@@ -18,7 +26,7 @@ Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-shell = "2.0.0-beta"
|
||||
tauri-plugin-shell = "2.0.0"
|
||||
# alternatively with Git:
|
||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
@@ -60,8 +68,8 @@ fn main() {
|
||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||
|
||||
```javascript
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
Command.create("git", ["commit", "-m", "the commit message"]);
|
||||
import { Command } from '@tauri-apps/plugin-shell'
|
||||
Command.create('git', ['commit', '-m', 'the commit message'])
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Security Policy
|
||||
|
||||
**Do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.**
|
||||
|
||||
Include as much of the following information:
|
||||
|
||||
- Type of issue (e.g. improper input parsing, privilege escalation, etc.)
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- The distribution affected or used to help us with reproduction of the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Ideally a reproduction repository
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
We prefer to receive reports in English.
|
||||
|
||||
## Contact
|
||||
|
||||
Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new).
|
||||
|
||||
Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app).
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,39 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.tauri.shell"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.shell
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.Plugin
|
||||
import java.io.File
|
||||
|
||||
@TauriPlugin
|
||||
class ShellPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
@Command
|
||||
fun open(invoke: Invoke) {
|
||||
try {
|
||||
val url = invoke.parseArgs(String::class.java)
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.applicationContext?.startActivity(intent)
|
||||
invoke.resolve()
|
||||
} catch (ex: Exception) {
|
||||
invoke.reject(ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_SHELL__=function(e){"use strict";function t(e,t,s,n){if("a"===s&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(e):n?n.value:t.get(e)}function s(e,t,s,n,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var n,i,r;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),r.set(this,{}),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:o})=>{if(o===t(this,i,"f")){s(this,i,o+1),t(this,n,"f").call(this,e);const a=Object.keys(t(this,r,"f"));if(a.length>0){let e=o+1;for(const s of a.sort()){if(parseInt(s)!==e)break;{const i=t(this,r,"f")[s];delete t(this,r,"f")[s],t(this,n,"f").call(this,i),e+=1}}s(this,i,e)}}else t(this,r,"f")[o.toString()]=e}))}set onmessage(e){s(this,n,e)}get onmessage(){return t(this,n,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function a(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}n=new WeakMap,i=new WeakMap,r=new WeakMap;class h{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=n=>{this.removeListener(e,s),t(n)};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=n=>{this.removeListener(e,s),t(n)};return this.prependListener(e,s)}}class c{constructor(e){this.pid=e}async write(e){await a("plugin:shell|stdin_write",{pid:this.pid,buffer:e})}async kill(){await a("plugin:shell|kill",{cmd:"killChild",pid:this.pid})}}class l extends h{constructor(e,t=[],s){super(),this.stdout=new h,this.stderr=new h,this.program=e,this.args="string"==typeof t?[t]:t,this.options=s??{}}static create(e,t=[],s){return new l(e,t,s)}static sidecar(e,t=[],s){const n=new l(e,t,s);return n.options.sidecar=!0,n}async spawn(){const e=this.program,t=this.args,s=this.options;"object"==typeof t&&Object.freeze(t);const n=new o;return n.onmessage=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)}},await a("plugin:shell|spawn",{program:e,args:t,options:s,onEvent:n}).then((e=>new c(e)))}async execute(){const e=this.program,t=this.args,s=this.options;return"object"==typeof t&&Object.freeze(t),await a("plugin:shell|execute",{program:e,args:t,options:s})}}return e.Child=c,e.Command=l,e.EventEmitter=h,e.open=async function(e,t){await a("plugin:shell|open",{path:e,with:t})},e}({});Object.defineProperty(window.__TAURI__,"shell",{value:__TAURI_PLUGIN_SHELL__})}
|
||||
+176
-2
@@ -2,13 +2,187 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
#[path = "src/scope_entry.rs"]
|
||||
mod scope_entry;
|
||||
|
||||
const COMMANDS: &[&str] = &["execute", "stdin_write", "kill", "open"];
|
||||
/// A command argument allowed to be executed by the webview API.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, schemars::JsonSchema)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellScopeEntryAllowedArg {
|
||||
/// 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.
|
||||
///
|
||||
/// The regex string is by default surrounded by `^...$` to match the full string.
|
||||
/// For example the `https?://\w+` regex would be registered as `^https?://\w+$`.
|
||||
///
|
||||
/// [regex]: <https://docs.rs/regex/latest/regex/#syntax>
|
||||
validator: String,
|
||||
|
||||
/// Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.
|
||||
///
|
||||
/// This means the regex will not match on the entire string by default, which might
|
||||
/// be exploited if your regex allow unexpected input to be considered valid.
|
||||
/// When using this option, make sure your regex is correct.
|
||||
#[serde(default)]
|
||||
raw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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 [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to
|
||||
/// be passed to the attached command configuration.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, JsonSchema)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellScopeEntryAllowedArgs {
|
||||
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
|
||||
Flag(bool),
|
||||
|
||||
/// A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.
|
||||
List(Vec<ShellScopeEntryAllowedArg>),
|
||||
}
|
||||
|
||||
impl Default for ShellScopeEntryAllowedArgs {
|
||||
fn default() -> Self {
|
||||
Self::Flag(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell scope entry.
|
||||
#[derive(JsonSchema)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[allow(unused)]
|
||||
pub(crate) enum ShellScopeEntry {
|
||||
Command {
|
||||
/// 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.
|
||||
name: String,
|
||||
/// 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`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||
// use default just so the schema doesn't flag it as required
|
||||
#[serde(rename = "cmd")]
|
||||
command: PathBuf,
|
||||
/// The allowed arguments for the command execution.
|
||||
#[serde(default)]
|
||||
args: ShellScopeEntryAllowedArgs,
|
||||
},
|
||||
Sidecar {
|
||||
/// 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.
|
||||
name: String,
|
||||
/// The allowed arguments for the command execution.
|
||||
#[serde(default)]
|
||||
args: ShellScopeEntryAllowedArgs,
|
||||
/// If this command is a sidecar command.
|
||||
sidecar: bool,
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure `ShellScopeEntry` and `scope_entry::EntryRaw`
|
||||
// and `ShellScopeEntryAllowedArg` and `ShellAllowedArg`
|
||||
// and `ShellScopeEntryAllowedArgs` and `ShellAllowedArgs`
|
||||
// are kept in sync
|
||||
#[allow(clippy::unnecessary_operation)]
|
||||
fn _f() {
|
||||
match (ShellScopeEntry::Sidecar {
|
||||
name: String::new(),
|
||||
args: ShellScopeEntryAllowedArgs::Flag(false),
|
||||
sidecar: true,
|
||||
}) {
|
||||
ShellScopeEntry::Command {
|
||||
name,
|
||||
command,
|
||||
args,
|
||||
} => scope_entry::EntryRaw {
|
||||
name,
|
||||
command: Some(command),
|
||||
args: match args {
|
||||
ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag),
|
||||
ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List(
|
||||
vec.into_iter()
|
||||
.map(|s| match s {
|
||||
ShellScopeEntryAllowedArg::Fixed(fixed) => {
|
||||
scope_entry::ShellAllowedArg::Fixed(fixed)
|
||||
}
|
||||
ShellScopeEntryAllowedArg::Var { validator, raw } => {
|
||||
scope_entry::ShellAllowedArg::Var { validator, raw }
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
sidecar: false,
|
||||
},
|
||||
ShellScopeEntry::Sidecar {
|
||||
name,
|
||||
args,
|
||||
sidecar,
|
||||
} => scope_entry::EntryRaw {
|
||||
name,
|
||||
command: None,
|
||||
args: match args {
|
||||
ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag),
|
||||
ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List(
|
||||
vec.into_iter()
|
||||
.map(|s| match s {
|
||||
ShellScopeEntryAllowedArg::Fixed(fixed) => {
|
||||
scope_entry::ShellAllowedArg::Fixed(fixed)
|
||||
}
|
||||
ShellScopeEntryAllowedArg::Var { validator, raw } => {
|
||||
scope_entry::ShellAllowedArg::Var { validator, raw }
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
sidecar,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const COMMANDS: &[&str] = &["execute", "spawn", "stdin_write", "kill", "open"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.global_scope_schema(schemars::schema_for!(scope_entry::Entry))
|
||||
.global_api_script_path("./api-iife.js")
|
||||
.global_scope_schema(schemars::schema_for!(ShellScopeEntry))
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let mobile = target_os == "ios" || target_os == "android";
|
||||
alias("desktop", !mobile);
|
||||
alias("mobile", mobile);
|
||||
}
|
||||
|
||||
// creates a cfg alias if `has_feature` is true.
|
||||
// `alias` must be a snake case string.
|
||||
fn alias(alias: &str, has_feature: bool) {
|
||||
if has_feature {
|
||||
println!("cargo:rustc-cfg={alias}");
|
||||
}
|
||||
}
|
||||
|
||||
+152
-197
@@ -18,11 +18,11 @@
|
||||
*
|
||||
* ### Restricting access to the {@link Command | `Command`} APIs
|
||||
*
|
||||
* The plugin configuration object has a `scope` field that defines an array of CLIs that can be used.
|
||||
* The plugin permissions object has a `scope` field that defines an array of CLIs that can be used.
|
||||
* Each CLI is a configuration object `{ name: string, cmd: string, sidecar?: bool, args?: boolean | Arg[] }`.
|
||||
*
|
||||
* - `name`: the unique identifier of the command, passed to the {@link Command.create | Command.create function}.
|
||||
* If it's a sidecar, this must be the value defined on `tauri.conf.json > tauri > bundle > externalBin`.
|
||||
* If it's a sidecar, this must be the value defined on `tauri.conf.json > bundle > externalBin`.
|
||||
* - `cmd`: the program that is executed on this configuration. If it's a sidecar, this value is ignored.
|
||||
* - `sidecar`: whether the object configures a sidecar or a system program.
|
||||
* - `args`: the arguments that can be passed to the program. By default no arguments are allowed.
|
||||
@@ -35,12 +35,13 @@
|
||||
*
|
||||
* CLI: `git commit -m "the commit message"`
|
||||
*
|
||||
* Configuration:
|
||||
* Capability:
|
||||
* ```json
|
||||
* {
|
||||
* "plugins": {
|
||||
* "shell": {
|
||||
* "scope": [
|
||||
* "permissions": [
|
||||
* {
|
||||
* "identifier": "shell:allow-execute",
|
||||
* "allow": [
|
||||
* {
|
||||
* "name": "run-git-commit",
|
||||
* "cmd": "git",
|
||||
@@ -48,7 +49,7 @@
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
* Usage:
|
||||
@@ -62,27 +63,27 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke, Channel } from "@tauri-apps/api/core";
|
||||
import { invoke, Channel } from '@tauri-apps/api/core'
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
interface SpawnOptions {
|
||||
/** Current working directory. */
|
||||
cwd?: string;
|
||||
cwd?: string
|
||||
/** Environment variables. set to `null` to clear the process env. */
|
||||
env?: Record<string, string>;
|
||||
env?: Record<string, string>
|
||||
/**
|
||||
* Character encoding for stdout/stderr
|
||||
*
|
||||
* @since 2.0.0
|
||||
* */
|
||||
encoding?: string;
|
||||
encoding?: string
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
interface InternalSpawnOptions extends SpawnOptions {
|
||||
sidecar?: boolean;
|
||||
sidecar?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,46 +91,13 @@ interface InternalSpawnOptions extends SpawnOptions {
|
||||
*/
|
||||
interface ChildProcess<O extends IOPayload> {
|
||||
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
||||
code: number | null;
|
||||
code: number | null
|
||||
/** If the process was terminated by a signal, represents that signal. */
|
||||
signal: number | null;
|
||||
signal: number | null
|
||||
/** The data that the process wrote to `stdout`. */
|
||||
stdout: O;
|
||||
stdout: O
|
||||
/** The data that the process wrote to `stderr`. */
|
||||
stderr: O;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a process.
|
||||
*
|
||||
* @ignore
|
||||
* @param program The name of the scoped command.
|
||||
* @param onEventHandler Event handler.
|
||||
* @param args Program arguments.
|
||||
* @param options Configuration for the process spawn.
|
||||
* @returns A promise resolving to the process id.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function execute<O extends IOPayload>(
|
||||
onEventHandler: (event: CommandEvent<O>) => void,
|
||||
program: string,
|
||||
args: string | string[] = [],
|
||||
options?: InternalSpawnOptions,
|
||||
): Promise<number> {
|
||||
if (typeof args === "object") {
|
||||
Object.freeze(args);
|
||||
}
|
||||
|
||||
const onEvent = new Channel<CommandEvent<O>>();
|
||||
onEvent.onmessage = onEventHandler;
|
||||
|
||||
return invoke<number>("plugin:shell|execute", {
|
||||
program,
|
||||
args,
|
||||
options,
|
||||
onEvent,
|
||||
});
|
||||
stderr: O
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +108,7 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
/** @ignore */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
private eventListeners: Record<keyof E, Array<(arg: any) => void>> =
|
||||
Object.create(null);
|
||||
Object.create(null)
|
||||
|
||||
/**
|
||||
* Alias for `emitter.on(eventName, listener)`.
|
||||
@@ -149,9 +117,9 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
addListener<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
return this.on(eventName, listener);
|
||||
return this.on(eventName, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,9 +129,9 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
removeListener<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
return this.off(eventName, listener);
|
||||
return this.off(eventName, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,16 +146,16 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
on<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
if (eventName in this.eventListeners) {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.eventListeners[eventName].push(listener);
|
||||
this.eventListeners[eventName].push(listener)
|
||||
} else {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.eventListeners[eventName] = [listener];
|
||||
this.eventListeners[eventName] = [listener]
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,14 +168,13 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
once<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
const wrapper = (arg: E[typeof eventName]): void => {
|
||||
this.removeListener(eventName, wrapper);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
listener(arg);
|
||||
};
|
||||
return this.addListener(eventName, wrapper);
|
||||
this.removeListener(eventName, wrapper)
|
||||
listener(arg)
|
||||
}
|
||||
return this.addListener(eventName, wrapper)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,15 +185,15 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
off<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
if (eventName in this.eventListeners) {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.eventListeners[eventName] = this.eventListeners[eventName].filter(
|
||||
(l) => l !== listener,
|
||||
);
|
||||
(l) => l !== listener
|
||||
)
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,13 +205,13 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
removeAllListeners<N extends keyof E>(event?: N): this {
|
||||
if (event) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete,security/detect-object-injection
|
||||
delete this.eventListeners[event];
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
delete this.eventListeners[event]
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.eventListeners = Object.create(null);
|
||||
this.eventListeners = Object.create(null)
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,13 +225,12 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
emit<N extends keyof E>(eventName: N, arg: E[typeof eventName]): boolean {
|
||||
if (eventName in this.eventListeners) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,security/detect-object-injection
|
||||
const listeners = this.eventListeners[eventName];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
for (const listener of listeners) listener(arg);
|
||||
return true;
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const listeners = this.eventListeners[eventName]
|
||||
for (const listener of listeners) listener(arg)
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,8 +241,8 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
listenerCount<N extends keyof E>(eventName: N): number {
|
||||
if (eventName in this.eventListeners)
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this.eventListeners[eventName].length;
|
||||
return 0;
|
||||
return this.eventListeners[eventName].length
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,16 +257,16 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
prependListener<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
if (eventName in this.eventListeners) {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.eventListeners[eventName].unshift(listener);
|
||||
this.eventListeners[eventName].unshift(listener)
|
||||
} else {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.eventListeners[eventName] = [listener];
|
||||
this.eventListeners[eventName] = [listener]
|
||||
}
|
||||
return this;
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,15 +279,15 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
prependOnceListener<N extends keyof E>(
|
||||
eventName: N,
|
||||
listener: (arg: E[typeof eventName]) => void,
|
||||
listener: (arg: E[typeof eventName]) => void
|
||||
): this {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wrapper = (arg: any): void => {
|
||||
this.removeListener(eventName, wrapper);
|
||||
this.removeListener(eventName, wrapper)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
listener(arg);
|
||||
};
|
||||
return this.prependListener(eventName, wrapper);
|
||||
listener(arg)
|
||||
}
|
||||
return this.prependListener(eventName, wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,10 +296,10 @@ class EventEmitter<E extends Record<string, any>> {
|
||||
*/
|
||||
class Child {
|
||||
/** The child process `pid`. */
|
||||
pid: number;
|
||||
pid: number
|
||||
|
||||
constructor(pid: number) {
|
||||
this.pid = pid;
|
||||
this.pid = pid
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,12 +319,11 @@ class Child {
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async write(data: IOPayload): Promise<void> {
|
||||
return invoke("plugin:shell|stdin_write", {
|
||||
async write(data: IOPayload | number[]): Promise<void> {
|
||||
await invoke('plugin:shell|stdin_write', {
|
||||
pid: this.pid,
|
||||
// correctly serialize Uint8Arrays
|
||||
buffer: typeof data === "string" ? data : Array.from(data),
|
||||
});
|
||||
buffer: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,20 +334,20 @@ class Child {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async kill(): Promise<void> {
|
||||
return invoke("plugin:shell|kill", {
|
||||
cmd: "killChild",
|
||||
pid: this.pid,
|
||||
});
|
||||
await invoke('plugin:shell|kill', {
|
||||
cmd: 'killChild',
|
||||
pid: this.pid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandEvents {
|
||||
close: TerminatedPayload;
|
||||
error: string;
|
||||
close: TerminatedPayload
|
||||
error: string
|
||||
}
|
||||
|
||||
interface OutputEvents<O extends IOPayload> {
|
||||
data: O;
|
||||
data: O
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,15 +373,15 @@ interface OutputEvents<O extends IOPayload> {
|
||||
*/
|
||||
class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
/** @ignore Program to execute. */
|
||||
private readonly program: string;
|
||||
private readonly program: string
|
||||
/** @ignore Program arguments */
|
||||
private readonly args: string[];
|
||||
private readonly args: string[]
|
||||
/** @ignore Spawn options. */
|
||||
private readonly options: InternalSpawnOptions;
|
||||
private readonly options: InternalSpawnOptions
|
||||
/** Event emitter for the `stdout`. Emits the `data` event. */
|
||||
readonly stdout = new EventEmitter<OutputEvents<O>>();
|
||||
readonly stdout = new EventEmitter<OutputEvents<O>>()
|
||||
/** Event emitter for the `stderr`. Emits the `data` event. */
|
||||
readonly stderr = new EventEmitter<OutputEvents<O>>();
|
||||
readonly stderr = new EventEmitter<OutputEvents<O>>()
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@@ -430,25 +395,25 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
private constructor(
|
||||
program: string,
|
||||
args: string | string[] = [],
|
||||
options?: SpawnOptions,
|
||||
options?: SpawnOptions
|
||||
) {
|
||||
super();
|
||||
this.program = program;
|
||||
this.args = typeof args === "string" ? [args] : args;
|
||||
this.options = options ?? {};
|
||||
super()
|
||||
this.program = program
|
||||
this.args = typeof args === 'string' ? [args] : args
|
||||
this.options = options ?? {}
|
||||
}
|
||||
|
||||
static create(program: string, args?: string | string[]): Command<string>;
|
||||
static create(program: string, args?: string | string[]): Command<string>
|
||||
static create(
|
||||
program: string,
|
||||
args?: string | string[],
|
||||
options?: SpawnOptions & { encoding: "raw" },
|
||||
): Command<Uint8Array>;
|
||||
options?: SpawnOptions & { encoding: 'raw' }
|
||||
): Command<Uint8Array>
|
||||
static create(
|
||||
program: string,
|
||||
args?: string | string[],
|
||||
options?: SpawnOptions,
|
||||
): Command<string>;
|
||||
options?: SpawnOptions
|
||||
): Command<string>
|
||||
|
||||
/**
|
||||
* Creates a command to execute the given program.
|
||||
@@ -465,22 +430,22 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
static create<O extends IOPayload>(
|
||||
program: string,
|
||||
args: string | string[] = [],
|
||||
options?: SpawnOptions,
|
||||
options?: SpawnOptions
|
||||
): Command<O> {
|
||||
return new Command(program, args, options);
|
||||
return new Command(program, args, options)
|
||||
}
|
||||
|
||||
static sidecar(program: string, args?: string | string[]): Command<string>;
|
||||
static sidecar(program: string, args?: string | string[]): Command<string>
|
||||
static sidecar(
|
||||
program: string,
|
||||
args?: string | string[],
|
||||
options?: SpawnOptions & { encoding: "raw" },
|
||||
): Command<Uint8Array>;
|
||||
options?: SpawnOptions & { encoding: 'raw' }
|
||||
): Command<Uint8Array>
|
||||
static sidecar(
|
||||
program: string,
|
||||
args?: string | string[],
|
||||
options?: SpawnOptions,
|
||||
): Command<string>;
|
||||
options?: SpawnOptions
|
||||
): Command<string>
|
||||
|
||||
/**
|
||||
* Creates a command to execute the given sidecar program.
|
||||
@@ -497,11 +462,11 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
static sidecar<O extends IOPayload>(
|
||||
program: string,
|
||||
args: string | string[] = [],
|
||||
options?: SpawnOptions,
|
||||
options?: SpawnOptions
|
||||
): Command<O> {
|
||||
const instance = new Command<O>(program, args, options);
|
||||
instance.options.sidecar = true;
|
||||
return instance;
|
||||
const instance = new Command<O>(program, args, options)
|
||||
instance.options.sidecar = true
|
||||
return instance
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,27 +477,38 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async spawn(): Promise<Child> {
|
||||
return execute<O>(
|
||||
(event) => {
|
||||
switch (event.event) {
|
||||
case "Error":
|
||||
this.emit("error", event.payload);
|
||||
break;
|
||||
case "Terminated":
|
||||
this.emit("close", event.payload);
|
||||
break;
|
||||
case "Stdout":
|
||||
this.stdout.emit("data", event.payload);
|
||||
break;
|
||||
case "Stderr":
|
||||
this.stderr.emit("data", event.payload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
this.program,
|
||||
this.args,
|
||||
this.options,
|
||||
).then((pid) => new Child(pid));
|
||||
const program = this.program
|
||||
const args = this.args
|
||||
const options = this.options
|
||||
|
||||
if (typeof args === 'object') {
|
||||
Object.freeze(args)
|
||||
}
|
||||
|
||||
const onEvent = new Channel<CommandEvent<O>>()
|
||||
onEvent.onmessage = (event) => {
|
||||
switch (event.event) {
|
||||
case 'Error':
|
||||
this.emit('error', event.payload)
|
||||
break
|
||||
case 'Terminated':
|
||||
this.emit('close', event.payload)
|
||||
break
|
||||
case 'Stdout':
|
||||
this.stdout.emit('data', event.payload)
|
||||
break
|
||||
case 'Stderr':
|
||||
this.stderr.emit('data', event.payload)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return await invoke<number>('plugin:shell|spawn', {
|
||||
program,
|
||||
args,
|
||||
options,
|
||||
onEvent
|
||||
}).then((pid) => new Child(pid))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -552,40 +528,19 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async execute(): Promise<ChildProcess<O>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.on("error", reject);
|
||||
const program = this.program
|
||||
const args = this.args
|
||||
const options = this.options
|
||||
|
||||
const stdout: O[] = [];
|
||||
const stderr: O[] = [];
|
||||
this.stdout.on("data", (line: O) => {
|
||||
stdout.push(line);
|
||||
});
|
||||
this.stderr.on("data", (line: O) => {
|
||||
stderr.push(line);
|
||||
});
|
||||
|
||||
this.on("close", (payload: TerminatedPayload) => {
|
||||
resolve({
|
||||
code: payload.code,
|
||||
signal: payload.signal,
|
||||
stdout: this.collectOutput(stdout) as O,
|
||||
stderr: this.collectOutput(stderr) as O,
|
||||
});
|
||||
});
|
||||
|
||||
this.spawn().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
private collectOutput(events: O[]): string | Uint8Array {
|
||||
if (this.options.encoding === "raw") {
|
||||
return events.reduce<Uint8Array>((p, c) => {
|
||||
return new Uint8Array([...p, ...(c as Uint8Array), 10]);
|
||||
}, new Uint8Array());
|
||||
} else {
|
||||
return events.join("\n");
|
||||
if (typeof args === 'object') {
|
||||
Object.freeze(args)
|
||||
}
|
||||
|
||||
return await invoke<ChildProcess<O>>('plugin:shell|execute', {
|
||||
program,
|
||||
args,
|
||||
options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,8 +548,8 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
|
||||
* Describes the event message received from the command.
|
||||
*/
|
||||
interface Event<T, V> {
|
||||
event: T;
|
||||
payload: V;
|
||||
event: T
|
||||
payload: V
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -602,20 +557,20 @@ interface Event<T, V> {
|
||||
*/
|
||||
interface TerminatedPayload {
|
||||
/** Exit code of the process. `null` if the process was terminated by a signal on Unix. */
|
||||
code: number | null;
|
||||
code: number | null
|
||||
/** If the process was terminated by a signal, represents that signal. */
|
||||
signal: number | null;
|
||||
signal: number | null
|
||||
}
|
||||
|
||||
/** Event payload type */
|
||||
type IOPayload = string | Uint8Array;
|
||||
type IOPayload = string | Uint8Array
|
||||
|
||||
/** Events emitted by the child process. */
|
||||
type CommandEvent<O extends IOPayload> =
|
||||
| Event<"Stdout", O>
|
||||
| Event<"Stderr", O>
|
||||
| Event<"Terminated", TerminatedPayload>
|
||||
| Event<"Error", string>;
|
||||
| Event<'Stdout', O>
|
||||
| Event<'Stderr', O>
|
||||
| Event<'Terminated', TerminatedPayload>
|
||||
| Event<'Error', string>
|
||||
|
||||
/**
|
||||
* Opens a path or URL with the system's default app,
|
||||
@@ -644,18 +599,18 @@ type CommandEvent<O extends IOPayload> =
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function open(path: string, openWith?: string): Promise<void> {
|
||||
return invoke("plugin:shell|open", {
|
||||
await invoke('plugin:shell|open', {
|
||||
path,
|
||||
with: openWith,
|
||||
});
|
||||
with: openWith
|
||||
})
|
||||
}
|
||||
|
||||
export { Command, Child, EventEmitter, open };
|
||||
export { Command, Child, EventEmitter, open }
|
||||
export type {
|
||||
IOPayload,
|
||||
CommandEvents,
|
||||
TerminatedPayload,
|
||||
OutputEvents,
|
||||
ChildProcess,
|
||||
SpawnOptions,
|
||||
};
|
||||
SpawnOptions
|
||||
}
|
||||
|
||||
@@ -2,39 +2,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// open <a href="..."> links with the API
|
||||
function openLinks() {
|
||||
document.querySelector("body")?.addEventListener("click", function (e) {
|
||||
let target = e.target as HTMLElement;
|
||||
while (target != null) {
|
||||
if (target.matches("a")) {
|
||||
const t = target as HTMLAnchorElement;
|
||||
function openLinks(): void {
|
||||
document.querySelector('body')?.addEventListener('click', function (e) {
|
||||
let target: HTMLElement | null = e.target as HTMLElement
|
||||
while (target) {
|
||||
if (target.matches('a')) {
|
||||
const t = target as HTMLAnchorElement
|
||||
if (
|
||||
t.href &&
|
||||
["http://", "https://", "mailto:", "tel:"].some((v) =>
|
||||
t.href.startsWith(v),
|
||||
t.href !== '' &&
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) =>
|
||||
t.href.startsWith(v)
|
||||
) &&
|
||||
t.target === "_blank"
|
||||
t.target === '_blank'
|
||||
) {
|
||||
invoke("plugin:shell|open", {
|
||||
path: t.href,
|
||||
});
|
||||
e.preventDefault();
|
||||
void invoke('plugin:shell|open', {
|
||||
path: t.href
|
||||
})
|
||||
e.preventDefault()
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
target = target.parentElement as HTMLElement;
|
||||
target = target.parentElement
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
document.readyState === 'complete' ||
|
||||
document.readyState === 'interactive'
|
||||
) {
|
||||
openLinks();
|
||||
openLinks()
|
||||
} else {
|
||||
window.addEventListener("DOMContentLoaded", openLinks, true);
|
||||
window.addEventListener('DOMContentLoaded', openLinks, true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "SwiftRs",
|
||||
"repositoryURL": "https://github.com/Brendonovich/swift-rs",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e",
|
||||
"version": "1.0.6"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.3
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-shell",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-shell",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-shell"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-shell",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
class ShellPlugin: Plugin {
|
||||
|
||||
@objc public func open(_ invoke: Invoke) throws {
|
||||
do {
|
||||
let urlString = try invoke.parseArgs(String.self)
|
||||
if let url = URL(string: urlString) {
|
||||
if #available(iOS 10, *) {
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
} else {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch {
|
||||
invoke.reject(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_shell")
|
||||
func initPlugin() -> Plugin {
|
||||
return ShellPlugin()
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-shell",
|
||||
"version": "2.0.0-beta.1",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.1",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"repository": "https://github.com/tauri-apps/plugins-workspace",
|
||||
"type": "module",
|
||||
"types": "./dist-js/index.d.ts",
|
||||
"main": "./dist-js/index.cjs",
|
||||
@@ -23,6 +24,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-beta.2"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-spawn"
|
||||
description = "Enables the spawn command without any pre-configured scope."
|
||||
commands.allow = ["spawn"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-spawn"
|
||||
description = "Denies the spawn command without any pre-configured scope."
|
||||
commands.deny = ["spawn"]
|
||||
@@ -1,34 +1,153 @@
|
||||
# Permissions
|
||||
## Default Permission
|
||||
|
||||
## allow-execute
|
||||
This permission set configures which
|
||||
shell functionality is exposed by default.
|
||||
|
||||
#### Granted Permissions
|
||||
|
||||
It allows to use the `open` functionality without any specific
|
||||
scope pre-configured. It will allow opening `http(s)://`,
|
||||
`tel:` and `mailto:` links.
|
||||
|
||||
|
||||
- `allow-open`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:allow-execute`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the execute command without any pre-configured scope.
|
||||
|
||||
## deny-execute
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:deny-execute`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the execute command without any pre-configured scope.
|
||||
|
||||
## allow-kill
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:allow-kill`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the kill command without any pre-configured scope.
|
||||
|
||||
## deny-kill
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:deny-kill`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the kill command without any pre-configured scope.
|
||||
|
||||
## allow-open
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:allow-open`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the open command without any pre-configured scope.
|
||||
|
||||
## deny-open
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:deny-open`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the open command without any pre-configured scope.
|
||||
|
||||
## allow-stdin-write
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:allow-spawn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the spawn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:deny-spawn`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the spawn command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:allow-stdin-write`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stdin_write command without any pre-configured scope.
|
||||
|
||||
## deny-stdin-write
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`shell:deny-stdin-write`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stdin_write command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
|
||||
[default]
|
||||
description = """
|
||||
This permission set configures which
|
||||
shell functionality is exposed by default.
|
||||
|
||||
#### Granted Permissions
|
||||
|
||||
It allows to use the `open` functionality without any specific
|
||||
scope pre-configured. It will allow opening `http(s)://`,
|
||||
`tel:` and `mailto:` links.
|
||||
"""
|
||||
|
||||
permissions = ["allow-open"]
|
||||
@@ -49,7 +49,7 @@
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -111,7 +111,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -136,6 +136,16 @@
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -162,7 +172,7 @@
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "A restriction of the command/endpoint functionality.\n\nIt can be of any serde serializable type and is used for allowing or preventing certain actions inside a Tauri command.\n\nThe scope is passed to the command and handled/enforced by the command itself.",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -176,7 +186,7 @@
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope.",
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
@@ -241,64 +251,103 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "allow-execute -> Enables the execute command without any pre-configured scope.",
|
||||
"description": "Enables the execute command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow-execute"
|
||||
]
|
||||
"const": "allow-execute"
|
||||
},
|
||||
{
|
||||
"description": "deny-execute -> Denies the execute command without any pre-configured scope.",
|
||||
"description": "Denies the execute command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"deny-execute"
|
||||
]
|
||||
"const": "deny-execute"
|
||||
},
|
||||
{
|
||||
"description": "allow-kill -> Enables the kill command without any pre-configured scope.",
|
||||
"description": "Enables the kill command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow-kill"
|
||||
]
|
||||
"const": "allow-kill"
|
||||
},
|
||||
{
|
||||
"description": "deny-kill -> Denies the kill command without any pre-configured scope.",
|
||||
"description": "Denies the kill command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"deny-kill"
|
||||
]
|
||||
"const": "deny-kill"
|
||||
},
|
||||
{
|
||||
"description": "allow-open -> Enables the open command without any pre-configured scope.",
|
||||
"description": "Enables the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow-open"
|
||||
]
|
||||
"const": "allow-open"
|
||||
},
|
||||
{
|
||||
"description": "deny-open -> Denies the open command without any pre-configured scope.",
|
||||
"description": "Denies the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"deny-open"
|
||||
]
|
||||
"const": "deny-open"
|
||||
},
|
||||
{
|
||||
"description": "allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.",
|
||||
"description": "Enables the spawn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"allow-stdin-write"
|
||||
]
|
||||
"const": "allow-spawn"
|
||||
},
|
||||
{
|
||||
"description": "deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.",
|
||||
"description": "Denies the spawn command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"deny-stdin-write"
|
||||
]
|
||||
"const": "deny-spawn"
|
||||
},
|
||||
{
|
||||
"description": "Enables the stdin_write command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-stdin-write"
|
||||
},
|
||||
{
|
||||
"description": "Denies the stdin_write command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-stdin-write"
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { createConfig } from "../../shared/rollup.config.js";
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import terser from "@rollup/plugin-terser";
|
||||
import { createConfig } from '../../shared/rollup.config.js'
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
|
||||
export default createConfig({
|
||||
additionalConfigs: {
|
||||
input: "guest-js/init.ts",
|
||||
input: 'guest-js/init.ts',
|
||||
output: {
|
||||
file: "src/init-iife.js",
|
||||
format: "iife",
|
||||
file: 'src/init-iife.js',
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [typescript(), terser(), nodeResolve()],
|
||||
onwarn: (warning) => {
|
||||
throw Object.assign(new Error(), warning);
|
||||
},
|
||||
},
|
||||
});
|
||||
throw Object.assign(new Error(), warning)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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