mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
refactor(core): use webview's URI schemes for IPC (#7170)
Co-authored-by: chip <chip@chip.sh>
This commit is contained in:
committed by
GitHub
parent
85efd0ae43
commit
fbeb5b9185
5
.changes/channel-rust.md
Normal file
5
.changes/channel-rust.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:enhance
|
||||
---
|
||||
|
||||
Added `Channel::new` allowing communication from a mobile plugin with Rust.
|
||||
6
.changes/ipc-custom-protocol.md
Normal file
6
.changes/ipc-custom-protocol.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": patch:enhance
|
||||
"tauri-utils": patch:enhance
|
||||
---
|
||||
|
||||
Use custom protocols on the IPC implementation to enhance performance.
|
||||
6
.changes/ipc-refactor.md
Normal file
6
.changes/ipc-refactor.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": patch:breaking
|
||||
"tauri-macros": patch:breaking
|
||||
---
|
||||
|
||||
Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types.
|
||||
5
.changes/linux-ipc-body-feature.md
Normal file
5
.changes/linux-ipc-body-feature.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:breaking
|
||||
---
|
||||
|
||||
Removed the `linux-protocol-headers` feature (now always enabled) and added `linux-ipc-protocol`.
|
||||
5
.changes/linux-protocol-body-feature.md
Normal file
5
.changes/linux-protocol-body-feature.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-runtime-wry": patch:breaking
|
||||
---
|
||||
|
||||
Removed the `linux-headers` feature (now always enabled) and added `linux-protocol-body`.
|
||||
6
.changes/migrate-csp.md
Normal file
6
.changes/migrate-csp.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-cli": patch:enhance
|
||||
"@tauri-apps/cli": patch:enhance
|
||||
---
|
||||
|
||||
Update migrate command to update the configuration CSP to include `ipc:` on the `connect-src` directive, needed by the new IPC using custom protocols.
|
||||
2
.github/workflows/lint-core.yml
vendored
2
.github/workflows/lint-core.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
clippy:
|
||||
- { args: '', key: 'empty' }
|
||||
- {
|
||||
args: '--features compression,wry,linux-protocol-headers,isolation,custom-protocol,system-tray,test',
|
||||
args: '--features compression,wry,isolation,custom-protocol,system-tray,test',
|
||||
key: 'all'
|
||||
}
|
||||
- { args: '--features custom-protocol', key: 'custom-protocol' }
|
||||
|
||||
7
.github/workflows/test-core.yml
vendored
7
.github/workflows/test-core.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
||||
key: no-default
|
||||
}
|
||||
- {
|
||||
args: --features compression,wry,linux-protocol-headers,isolation,custom-protocol,system-tray,test,
|
||||
args: --features compression,wry,isolation,custom-protocol,system-tray,test,
|
||||
key: all
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ jobs:
|
||||
workspaces: core -> ../target
|
||||
save-if: ${{ matrix.features.key == 'all' }}
|
||||
|
||||
- name: Downgrade crates with MSRV conflict
|
||||
# The --precise flag can only be used once per invocation.
|
||||
run: |
|
||||
cargo update -p time --precise 0.3.23
|
||||
|
||||
- name: test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
|
||||
@@ -211,7 +211,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
use #root::command::private::*;
|
||||
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
|
||||
#[allow(unused_variables)]
|
||||
let #root::Invoke { message: #message, resolver: #resolver } = $invoke;
|
||||
let #root::ipc::Invoke { message: #message, resolver: #resolver } = $invoke;
|
||||
|
||||
#body
|
||||
}};
|
||||
|
||||
@@ -48,4 +48,4 @@ macos-private-api = [
|
||||
"tauri-runtime/macos-private-api"
|
||||
]
|
||||
objc-exception = [ "wry/objc-exception" ]
|
||||
linux-headers = [ ]
|
||||
linux-protocol-body = [ "wry/linux-body", "webkit2gtk/v2_40" ]
|
||||
|
||||
@@ -3085,17 +3085,14 @@ fn create_webview<T: UserEvent>(
|
||||
webview_attributes,
|
||||
uri_scheme_protocols,
|
||||
mut window_builder,
|
||||
ipc_handler,
|
||||
label,
|
||||
ipc_handler,
|
||||
url,
|
||||
menu_ids,
|
||||
#[cfg(target_os = "android")]
|
||||
on_webview_created,
|
||||
..
|
||||
} = pending;
|
||||
let webview_id_map = context.webview_id_map.clone();
|
||||
#[cfg(windows)]
|
||||
let proxy = context.proxy.clone();
|
||||
|
||||
let window_event_listeners = WindowEventListeners::default();
|
||||
|
||||
@@ -3108,6 +3105,8 @@ fn create_webview<T: UserEvent>(
|
||||
|
||||
#[cfg(windows)]
|
||||
let window_theme = window_builder.inner.window.preferred_theme;
|
||||
#[cfg(windows)]
|
||||
let proxy = context.proxy.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -3130,7 +3129,7 @@ fn create_webview<T: UserEvent>(
|
||||
};
|
||||
let window = window_builder.inner.build(event_loop).unwrap();
|
||||
|
||||
webview_id_map.insert(window.id(), window_id);
|
||||
context.webview_id_map.insert(window.id(), window_id);
|
||||
|
||||
if window_builder.center {
|
||||
let _ = center_window(&window, window.inner_size());
|
||||
@@ -3157,17 +3156,18 @@ fn create_webview<T: UserEvent>(
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
|
||||
webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
|
||||
}
|
||||
{
|
||||
if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
|
||||
webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(theme) = window_theme {
|
||||
webview_builder = webview_builder.with_theme(match theme {
|
||||
WryTheme::Dark => wry::webview::Theme::Dark,
|
||||
WryTheme::Light => wry::webview::Theme::Light,
|
||||
_ => wry::webview::Theme::Light,
|
||||
});
|
||||
if let Some(theme) = window_theme {
|
||||
webview_builder = webview_builder.with_theme(match theme {
|
||||
WryTheme::Dark => wry::webview::Theme::Dark,
|
||||
WryTheme::Light => wry::webview::Theme::Light,
|
||||
_ => wry::webview::Theme::Light,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(handler) = ipc_handler {
|
||||
@@ -3178,6 +3178,7 @@ fn create_webview<T: UserEvent>(
|
||||
handler,
|
||||
));
|
||||
}
|
||||
|
||||
for (scheme, protocol) in uri_scheme_protocols {
|
||||
webview_builder = webview_builder.with_custom_protocol(scheme, move |wry_request| {
|
||||
protocol(&HttpRequestWrapper::from(wry_request).0)
|
||||
@@ -3254,7 +3255,7 @@ fn create_webview<T: UserEvent>(
|
||||
unsafe {
|
||||
controller.add_GotFocus(
|
||||
&FocusChangedEventHandler::create(Box::new(move |_, _| {
|
||||
let _ = proxy_.send_event(Message::Webview(
|
||||
let _ = proxy.send_event(Message::Webview(
|
||||
window_id,
|
||||
WebviewMessage::WebviewEvent(WebviewEvent::Focused(true)),
|
||||
));
|
||||
@@ -3267,7 +3268,7 @@ fn create_webview<T: UserEvent>(
|
||||
unsafe {
|
||||
controller.add_LostFocus(
|
||||
&FocusChangedEventHandler::create(Box::new(move |_, _| {
|
||||
let _ = proxy.send_event(Message::Webview(
|
||||
let _ = proxy_.send_event(Message::Webview(
|
||||
window_id,
|
||||
WebviewMessage::WebviewEvent(WebviewEvent::Focused(false)),
|
||||
));
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* isolation frame -> main frame = isolation message
|
||||
*/
|
||||
|
||||
;(async function () {
|
||||
;
|
||||
(async function () {
|
||||
/**
|
||||
* Sends the message to the isolation frame.
|
||||
* @param {any} message
|
||||
@@ -38,34 +39,52 @@
|
||||
* @return {Promise<{nonce: number[], payload: number[]}>}
|
||||
*/
|
||||
async function encrypt(data) {
|
||||
let algorithm = Object.create(null)
|
||||
const algorithm = Object.create(null)
|
||||
algorithm.name = 'AES-GCM'
|
||||
algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
|
||||
|
||||
let encoder = new TextEncoder()
|
||||
let payloadRaw = encoder.encode(__RAW_stringify_ipc_message_fn__(data))
|
||||
const encoder = new TextEncoder()
|
||||
const encoded = encoder.encode(__RAW_process_ipc_message_fn__(data).data)
|
||||
|
||||
return window.crypto.subtle
|
||||
.encrypt(algorithm, aesGcmKey, payloadRaw)
|
||||
.encrypt(algorithm, aesGcmKey, encoded)
|
||||
.then((payload) => {
|
||||
let result = Object.create(null)
|
||||
const result = Object.create(null)
|
||||
result.nonce = Array.from(new Uint8Array(algorithm.iv))
|
||||
result.payload = Array.from(new Uint8Array(payload))
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a message event is a valid isolation message.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
|
||||
* @return {boolean} - if the event was a valid isolation message
|
||||
*/
|
||||
function isIsolationMessage(data) {
|
||||
if (typeof data === 'object' && typeof data.payload === 'object') {
|
||||
const keys = data.payload ? Object.keys(data.payload) : []
|
||||
return (
|
||||
keys.length > 0 &&
|
||||
keys.every((key) => key === 'nonce' || key === 'payload')
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a message event is a valid isolation payload.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation payload
|
||||
* @return boolean
|
||||
*/
|
||||
function isIsolationPayload(event) {
|
||||
function isIsolationPayload(data) {
|
||||
return (
|
||||
typeof event.data === 'object' &&
|
||||
'callback' in event.data &&
|
||||
'error' in event.data
|
||||
typeof data === 'object' &&
|
||||
'callback' in data &&
|
||||
'error' in data &&
|
||||
!isIsolationMessage(data)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,7 +93,7 @@
|
||||
* @param {MessageEvent<any>} event
|
||||
*/
|
||||
async function payloadHandler(event) {
|
||||
if (!isIsolationPayload(event)) {
|
||||
if (!isIsolationPayload(event.data)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,8 +104,13 @@
|
||||
data = await window.__TAURI_ISOLATION_HOOK__(data)
|
||||
}
|
||||
|
||||
const encrypted = await encrypt(data)
|
||||
sendMessage(encrypted)
|
||||
const message = Object.create(null)
|
||||
message.cmd = data.cmd
|
||||
message.callback = data.callback
|
||||
message.error = data.error
|
||||
message.options = data.options
|
||||
message.payload = await encrypt(data.payload)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
window.addEventListener('message', payloadHandler, false)
|
||||
|
||||
@@ -96,16 +96,14 @@ impl Keys {
|
||||
}
|
||||
|
||||
/// Decrypts a message using the generated keys.
|
||||
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<String, Error> {
|
||||
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
|
||||
let RawIsolationPayload { nonce, payload } = raw;
|
||||
let nonce: [u8; 12] = nonce.as_ref().try_into()?;
|
||||
let bytes = self
|
||||
self
|
||||
.aes_gcm
|
||||
.key
|
||||
.decrypt(Nonce::from_slice(&nonce), payload.as_ref())
|
||||
.map_err(|_| self::Error::Aes)?;
|
||||
|
||||
String::from_utf8(bytes).map_err(Into::into)
|
||||
.map_err(|_| self::Error::Aes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,11 +114,11 @@ pub struct RawIsolationPayload<'a> {
|
||||
payload: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for RawIsolationPayload<'a> {
|
||||
impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
serde_json::from_str(value).map_err(Into::into)
|
||||
fn try_from(value: &'a Vec<u8>) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(value).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,9 +139,9 @@ pub struct IsolationJavascriptCodegen {
|
||||
pub struct IsolationJavascriptRuntime<'a> {
|
||||
/// The key used on the Rust backend and the Isolation Javascript
|
||||
pub runtime_aes_gcm_key: &'a [u8; 32],
|
||||
/// The function that stringifies a IPC message.
|
||||
/// The function that processes the IPC message.
|
||||
#[raw]
|
||||
pub stringify_ipc_message_fn: &'a str,
|
||||
pub process_ipc_message_fn: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -61,6 +61,7 @@ reqwest = { version = "0.11", default-features = false, features = [ "json", "st
|
||||
bytes = { version = "1", features = [ "serde" ] }
|
||||
raw-window-handle = "0.5"
|
||||
glob = "0.3"
|
||||
mime = "0.3"
|
||||
data-url = { version = "0.2", optional = true }
|
||||
serialize-to-javascript = "=0.1.1"
|
||||
infer = { version = "0.9", optional = true }
|
||||
@@ -118,7 +119,7 @@ test = [ ]
|
||||
compression = [ "tauri-macros/compression", "tauri-utils/compression" ]
|
||||
wry = [ "tauri-runtime-wry" ]
|
||||
objc-exception = [ "tauri-runtime-wry/objc-exception" ]
|
||||
linux-protocol-headers = [ "tauri-runtime-wry/linux-headers", "webkit2gtk/v2_36" ]
|
||||
linux-ipc-protocol = [ "tauri-runtime-wry/linux-protocol-body", "webkit2gtk/v2_40" ]
|
||||
isolation = [ "tauri-utils/isolation", "tauri-macros/isolation" ]
|
||||
custom-protocol = [ "tauri-macros/custom-protocol" ]
|
||||
native-tls = [ "reqwest/native-tls" ]
|
||||
|
||||
@@ -50,6 +50,11 @@ fn main() {
|
||||
alias("desktop", !mobile);
|
||||
alias("mobile", mobile);
|
||||
|
||||
alias(
|
||||
"ipc_custom_protocol",
|
||||
target_os != "android" && (target_os != "linux" || has_feature("linux-ipc-protocol")),
|
||||
);
|
||||
|
||||
let checked_features_out_path = Path::new(&var("OUT_DIR").unwrap()).join("checked_features");
|
||||
std::fs::write(
|
||||
checked_features_out_path,
|
||||
|
||||
@@ -14,6 +14,7 @@ class Invoke(
|
||||
val callback: Long,
|
||||
val error: Long,
|
||||
private val sendResponse: (callback: Long, data: PluginResult?) -> Unit,
|
||||
private val sendChannelData: (channelId: Long, data: PluginResult) -> Unit,
|
||||
val data: JSObject) {
|
||||
|
||||
fun resolve(data: JSObject?) {
|
||||
@@ -205,6 +206,6 @@ class Invoke(
|
||||
fun getChannel(name: String): Channel? {
|
||||
val channelDef = getString(name, "")
|
||||
val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null
|
||||
return Channel(callback) { res -> sendResponse(callback, PluginResult(res)) }
|
||||
return Channel(callback) { res -> sendChannelData(callback, PluginResult(res)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,15 +85,6 @@ class PluginManager(val activity: AppCompatActivity) {
|
||||
}
|
||||
}
|
||||
|
||||
@JniMethod
|
||||
fun postIpcMessage(webView: WebView, pluginId: String, command: String, data: JSObject, callback: Long, error: Long) {
|
||||
val invoke = Invoke(callback, command, callback, error, { fn, result ->
|
||||
webView.evaluateJavascript("window['_$fn']($result)", null)
|
||||
}, data)
|
||||
|
||||
dispatchPluginMessage(invoke, pluginId)
|
||||
}
|
||||
|
||||
@JniMethod
|
||||
fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) {
|
||||
val successId = 0L
|
||||
@@ -107,6 +98,8 @@ class PluginManager(val activity: AppCompatActivity) {
|
||||
error = result
|
||||
}
|
||||
handlePluginResponse(id, success?.toString(), error?.toString())
|
||||
}, { channelId, payload ->
|
||||
sendChannelData(channelId, payload.toString())
|
||||
}, data)
|
||||
|
||||
dispatchPluginMessage(invoke, pluginId)
|
||||
@@ -140,4 +133,5 @@ class PluginManager(val activity: AppCompatActivity) {
|
||||
}
|
||||
|
||||
private external fun handlePluginResponse(id: Int, success: String?, error: String?)
|
||||
private external fun sendChannelData(id: Long, data: String)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ public class Channel {
|
||||
public let id: UInt64
|
||||
let handler: (JsonValue) -> Void
|
||||
|
||||
public init(callback: UInt64, handler: @escaping (JsonValue) -> Void) {
|
||||
self.id = callback
|
||||
public init(id: UInt64, handler: @escaping (JsonValue) -> Void) {
|
||||
self.id = id
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
|
||||
@@ -8,76 +8,92 @@ import UIKit
|
||||
let CHANNEL_PREFIX = "__CHANNEL__:"
|
||||
|
||||
@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
|
||||
public var dictionaryRepresentation: NSDictionary {
|
||||
return data as NSDictionary
|
||||
}
|
||||
public var dictionaryRepresentation: NSDictionary {
|
||||
return data as NSDictionary
|
||||
}
|
||||
|
||||
public static var jsDateFormatter: ISO8601DateFormatter = {
|
||||
return ISO8601DateFormatter()
|
||||
}()
|
||||
public static var jsDateFormatter: ISO8601DateFormatter = {
|
||||
return ISO8601DateFormatter()
|
||||
}()
|
||||
|
||||
public var command: String
|
||||
var callback: UInt64
|
||||
var error: UInt64
|
||||
public var data: JSObject
|
||||
var sendResponse: (UInt64, JsonValue?) -> Void
|
||||
public var data: JSObject
|
||||
var sendResponse: (UInt64, JsonValue?) -> Void
|
||||
var sendChannelData: (UInt64, JsonValue) -> Void
|
||||
|
||||
public init(command: String, callback: UInt64, error: UInt64, sendResponse: @escaping (UInt64, JsonValue?) -> Void, data: JSObject?) {
|
||||
public init(
|
||||
command: String, callback: UInt64, error: UInt64,
|
||||
sendResponse: @escaping (UInt64, JsonValue?) -> Void,
|
||||
sendChannelData: @escaping (UInt64, JsonValue) -> Void, data: JSObject?
|
||||
) {
|
||||
self.command = command
|
||||
self.callback = callback
|
||||
self.error = error
|
||||
self.data = data ?? [:]
|
||||
self.sendResponse = sendResponse
|
||||
}
|
||||
self.data = data ?? [:]
|
||||
self.sendResponse = sendResponse
|
||||
self.sendChannelData = sendChannelData
|
||||
}
|
||||
|
||||
public func resolve() {
|
||||
sendResponse(callback, nil)
|
||||
}
|
||||
public func resolve() {
|
||||
sendResponse(callback, nil)
|
||||
}
|
||||
|
||||
public func resolve(_ data: JsonObject) {
|
||||
resolve(.dictionary(data))
|
||||
}
|
||||
public func resolve(_ data: JsonObject) {
|
||||
resolve(.dictionary(data))
|
||||
}
|
||||
|
||||
public func resolve(_ data: JsonValue) {
|
||||
sendResponse(callback, data)
|
||||
}
|
||||
public func resolve(_ data: JsonValue) {
|
||||
sendResponse(callback, data)
|
||||
}
|
||||
|
||||
public func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil) {
|
||||
let payload: NSMutableDictionary = ["message": message, "code": code ?? "", "error": error ?? ""]
|
||||
if let data = data {
|
||||
switch data {
|
||||
case .dictionary(let dict):
|
||||
for entry in dict {
|
||||
payload[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
}
|
||||
sendResponse(self.error, .dictionary(payload as! JsonObject))
|
||||
}
|
||||
public func reject(
|
||||
_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil
|
||||
) {
|
||||
let payload: NSMutableDictionary = [
|
||||
"message": message, "code": code ?? "", "error": error ?? "",
|
||||
]
|
||||
if let data = data {
|
||||
switch data {
|
||||
case .dictionary(let dict):
|
||||
for entry in dict {
|
||||
payload[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
}
|
||||
sendResponse(self.error, .dictionary(payload as! JsonObject))
|
||||
}
|
||||
|
||||
public func unimplemented() {
|
||||
unimplemented("not implemented")
|
||||
}
|
||||
public func unimplemented() {
|
||||
unimplemented("not implemented")
|
||||
}
|
||||
|
||||
public func unimplemented(_ message: String) {
|
||||
sendResponse(error, .dictionary(["message": message]))
|
||||
}
|
||||
public func unimplemented(_ message: String) {
|
||||
sendResponse(error, .dictionary(["message": message]))
|
||||
}
|
||||
|
||||
public func unavailable() {
|
||||
unavailable("not available")
|
||||
}
|
||||
public func unavailable() {
|
||||
unavailable("not available")
|
||||
}
|
||||
|
||||
public func unavailable(_ message: String) {
|
||||
sendResponse(error, .dictionary(["message": message]))
|
||||
}
|
||||
public func unavailable(_ message: String) {
|
||||
sendResponse(error, .dictionary(["message": message]))
|
||||
}
|
||||
|
||||
public func getChannel(_ key: String) -> Channel? {
|
||||
let channelDef = getString(key, "")
|
||||
guard let callback = UInt64(channelDef.components(separatedBy: CHANNEL_PREFIX)[1]) else {
|
||||
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
|
||||
if components.count < 2 {
|
||||
return nil
|
||||
}
|
||||
return Channel(callback: callback, handler: { (res: JsonValue) -> Void in
|
||||
self.sendResponse(callback, res)
|
||||
})
|
||||
guard let channelId = UInt64(components[1]) else {
|
||||
return nil
|
||||
}
|
||||
return Channel(
|
||||
id: channelId,
|
||||
handler: { (res: JsonValue) -> Void in
|
||||
self.sendChannelData(channelId, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,145 +2,147 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import SwiftRs
|
||||
import Foundation
|
||||
import SwiftRs
|
||||
import UIKit
|
||||
import WebKit
|
||||
import os.log
|
||||
|
||||
class PluginHandle {
|
||||
var instance: Plugin
|
||||
var loaded = false
|
||||
var instance: Plugin
|
||||
var loaded = false
|
||||
|
||||
init(plugin: Plugin) {
|
||||
instance = plugin
|
||||
}
|
||||
init(plugin: Plugin) {
|
||||
instance = plugin
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginManager {
|
||||
static let shared: PluginManager = PluginManager()
|
||||
public var viewController: UIViewController?
|
||||
var plugins: [String: PluginHandle] = [:]
|
||||
var ipcDispatchQueue = DispatchQueue(label: "ipc")
|
||||
public var isSimEnvironment: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
static let shared: PluginManager = PluginManager()
|
||||
public var viewController: UIViewController?
|
||||
var plugins: [String: PluginHandle] = [:]
|
||||
var ipcDispatchQueue = DispatchQueue(label: "ipc")
|
||||
public var isSimEnvironment: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
public func assetUrl(fromLocalURL url: URL?) -> URL? {
|
||||
guard let inputURL = url else {
|
||||
return nil
|
||||
}
|
||||
public func assetUrl(fromLocalURL url: URL?) -> URL? {
|
||||
guard let inputURL = url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
|
||||
}
|
||||
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
|
||||
}
|
||||
|
||||
func onWebviewCreated(_ webview: WKWebView) {
|
||||
for (_, handle) in plugins {
|
||||
if (!handle.loaded) {
|
||||
handle.instance.load(webview: webview)
|
||||
}
|
||||
}
|
||||
}
|
||||
func onWebviewCreated(_ webview: WKWebView) {
|
||||
for (_, handle) in plugins {
|
||||
if !handle.loaded {
|
||||
handle.instance.load(webview: webview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func load<P: Plugin>(name: String, plugin: P, config: JSObject, webview: WKWebView?) {
|
||||
func load<P: Plugin>(name: String, plugin: P, config: JSObject, webview: WKWebView?) {
|
||||
plugin.setConfig(config)
|
||||
let handle = PluginHandle(plugin: plugin)
|
||||
if let webview = webview {
|
||||
handle.instance.load(webview: webview)
|
||||
handle.loaded = true
|
||||
}
|
||||
plugins[name] = handle
|
||||
}
|
||||
let handle = PluginHandle(plugin: plugin)
|
||||
if let webview = webview {
|
||||
handle.instance.load(webview: webview)
|
||||
handle.loaded = true
|
||||
}
|
||||
plugins[name] = handle
|
||||
}
|
||||
|
||||
func invoke(name: String, invoke: Invoke) {
|
||||
if let plugin = plugins[name] {
|
||||
ipcDispatchQueue.async {
|
||||
let selectorWithThrows = Selector(("\(invoke.command):error:"))
|
||||
if plugin.instance.responds(to: selectorWithThrows) {
|
||||
var error: NSError? = nil
|
||||
withUnsafeMutablePointer(to: &error) {
|
||||
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
|
||||
unsafeBitCast(methodIMP, to: (@convention(c)(Any?, Selector, Invoke, OpaquePointer) -> Void).self)(plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
|
||||
}
|
||||
if let error = error {
|
||||
invoke.reject("\(error)")
|
||||
// TODO: app crashes without this leak
|
||||
let _ = Unmanaged.passRetained(error)
|
||||
}
|
||||
} else {
|
||||
let selector = Selector(("\(invoke.command):"))
|
||||
if plugin.instance.responds(to: selector) {
|
||||
plugin.instance.perform(selector, with: invoke)
|
||||
} else {
|
||||
invoke.reject("No command \(invoke.command) found for plugin \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Plugin \(name) not initialized")
|
||||
}
|
||||
}
|
||||
func invoke(name: String, invoke: Invoke) {
|
||||
if let plugin = plugins[name] {
|
||||
ipcDispatchQueue.async {
|
||||
let selectorWithThrows = Selector(("\(invoke.command):error:"))
|
||||
if plugin.instance.responds(to: selectorWithThrows) {
|
||||
var error: NSError? = nil
|
||||
withUnsafeMutablePointer(to: &error) {
|
||||
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
|
||||
unsafeBitCast(
|
||||
methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
|
||||
plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
|
||||
}
|
||||
if let error = error {
|
||||
invoke.reject("\(error)")
|
||||
// TODO: app crashes without this leak
|
||||
let _ = Unmanaged.passRetained(error)
|
||||
}
|
||||
} else {
|
||||
let selector = Selector(("\(invoke.command):"))
|
||||
if plugin.instance.responds(to: selector) {
|
||||
plugin.instance.perform(selector, with: invoke)
|
||||
} else {
|
||||
invoke.reject("No command \(invoke.command) found for plugin \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Plugin \(name) not initialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager: NSCopying {
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("register_plugin")
|
||||
func registerPlugin(name: SRString, plugin: NSObject, config: NSDictionary?, webview: WKWebView?) {
|
||||
PluginManager.shared.load(
|
||||
name: name.toString(),
|
||||
plugin: plugin as! Plugin,
|
||||
PluginManager.shared.load(
|
||||
name: name.toString(),
|
||||
plugin: plugin as! Plugin,
|
||||
config: JSTypes.coerceDictionaryToJSObject(config ?? [:], formattingDatesAsStrings: true)!,
|
||||
webview: webview
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@_cdecl("on_webview_created")
|
||||
func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
|
||||
PluginManager.shared.viewController = viewController
|
||||
PluginManager.shared.onWebviewCreated(webview)
|
||||
PluginManager.shared.viewController = viewController
|
||||
PluginManager.shared.onWebviewCreated(webview)
|
||||
}
|
||||
|
||||
@_cdecl("post_ipc_message")
|
||||
func postIpcMessage(webview: WKWebView, name: SRString, command: SRString, data: NSDictionary, callback: UInt64, error: UInt64) {
|
||||
let invoke = Invoke(command: command.toString(), callback: callback, error: error, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
|
||||
var payloadJson: String
|
||||
do {
|
||||
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
|
||||
} catch {
|
||||
payloadJson = "`\(error)`"
|
||||
}
|
||||
webview.evaluateJavaScript("window['_\(fn)'](\(payloadJson))")
|
||||
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
|
||||
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
|
||||
}
|
||||
|
||||
@_cdecl("run_plugin_method")
|
||||
@_cdecl("run_plugin_command")
|
||||
func runCommand(
|
||||
id: Int,
|
||||
name: SRString,
|
||||
command: SRString,
|
||||
data: NSDictionary,
|
||||
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void
|
||||
id: Int,
|
||||
name: SRString,
|
||||
command: SRString,
|
||||
data: NSDictionary,
|
||||
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>?) -> Void,
|
||||
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
|
||||
) {
|
||||
let callbackId: UInt64 = 0
|
||||
let errorId: UInt64 = 1
|
||||
let invoke = Invoke(command: command.toString(), callback: callbackId, error: errorId, sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
|
||||
let success = fn == callbackId
|
||||
var payloadJson: String = ""
|
||||
do {
|
||||
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
|
||||
} catch {
|
||||
payloadJson = "`\(error)`"
|
||||
}
|
||||
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
|
||||
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
|
||||
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
|
||||
let invoke = Invoke(
|
||||
command: command.toString(), callback: callbackId, error: errorId,
|
||||
sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in
|
||||
let success = fn == callbackId
|
||||
var payloadJson: String = ""
|
||||
do {
|
||||
try payloadJson =
|
||||
payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
|
||||
} catch {
|
||||
payloadJson = "`\(error)`"
|
||||
}
|
||||
callback(id, success, payloadJson.cString(using: String.Encoding.utf8))
|
||||
},
|
||||
sendChannelData: { (id: UInt64, payload: JsonValue) -> Void in
|
||||
var payloadJson: String = ""
|
||||
do {
|
||||
try payloadJson =
|
||||
payload.jsonRepresentation() ?? "`Failed to serialize payload`"
|
||||
} catch {
|
||||
payloadJson = "`\(error)`"
|
||||
}
|
||||
sendChannelData(id, payloadJson)
|
||||
}, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true))
|
||||
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,6 +13,15 @@
|
||||
})
|
||||
}
|
||||
|
||||
const osName = __TEMPLATE_os_name__
|
||||
|
||||
window.__TAURI__.convertFileSrc = function convertFileSrc(filePath, protocol = 'asset') {
|
||||
const path = encodeURIComponent(filePath)
|
||||
return osName === 'windows' || osName === 'android'
|
||||
? `https://${protocol}.localhost/${path}`
|
||||
: `${protocol}://localhost/${path}`
|
||||
}
|
||||
|
||||
window.__TAURI__.transformCallback = function transformCallback(
|
||||
callback,
|
||||
once
|
||||
@@ -48,7 +57,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.__TAURI_INVOKE__ = function invoke(cmd, args = {}) {
|
||||
window.__TAURI_INVOKE__ = function invoke(cmd, payload = {}, options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var callback = window.__TAURI__.transformCallback(function (r) {
|
||||
resolve(r)
|
||||
@@ -59,19 +68,13 @@
|
||||
delete window[`_${callback}`]
|
||||
}, true)
|
||||
|
||||
if (typeof cmd === 'string') {
|
||||
args.cmd = cmd
|
||||
} else if (typeof cmd === 'object') {
|
||||
args = cmd
|
||||
} else {
|
||||
return reject(new Error('Invalid argument type.'))
|
||||
}
|
||||
|
||||
const action = () => {
|
||||
window.__TAURI_IPC__({
|
||||
...args,
|
||||
cmd,
|
||||
callback,
|
||||
error: error
|
||||
error,
|
||||
payload,
|
||||
options
|
||||
})
|
||||
}
|
||||
if (window.__TAURI_IPC__) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
52
core/tauri/scripts/ipc-protocol.js
Normal file
52
core/tauri/scripts/ipc-protocol.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
;(function () {
|
||||
const processIpcMessage = __RAW_process_ipc_message_fn__
|
||||
const osName = __TEMPLATE_os_name__
|
||||
const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__
|
||||
const useCustomProtocol = __TEMPLATE_use_custom_protocol__
|
||||
|
||||
Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {
|
||||
value: (message) => {
|
||||
const { cmd, callback, error, payload, options } = message
|
||||
|
||||
// use custom protocol for IPC if the flag is set to true, the command is the fetch data command or when not on Linux/Android
|
||||
if (useCustomProtocol || cmd === fetchChannelDataCommand || (osName !== 'linux' && osName !== 'android')) {
|
||||
const { contentType, data } = processIpcMessage(payload)
|
||||
fetch(window.__TAURI__.convertFileSrc(cmd, 'ipc'), {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Tauri-Callback': callback,
|
||||
'Tauri-Error': error,
|
||||
...options?.headers
|
||||
}
|
||||
}).then((response) => {
|
||||
const cb = response.ok ? callback : error
|
||||
// we need to split here because on Android the content-type gets duplicated
|
||||
switch ((response.headers.get('content-type') || '').split(',')[0]) {
|
||||
case 'application/json':
|
||||
return response.json().then((r) => [cb, r])
|
||||
case 'text/plain':
|
||||
return response.text().then((r) => [cb, r])
|
||||
default:
|
||||
return response.arrayBuffer().then((r) => [cb, r])
|
||||
}
|
||||
}).then(([cb, data]) => {
|
||||
if (window[`_${cb}`]) {
|
||||
window[`_${cb}`](data)
|
||||
} else {
|
||||
console.warn(`[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// otherwise use the postMessage interface
|
||||
const { data } = processIpcMessage({ cmd, callback, error, options, ...payload })
|
||||
window.ipc.postMessage(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
@@ -33,11 +33,14 @@
|
||||
* @return {boolean} - if the event was a valid isolation message
|
||||
*/
|
||||
function isIsolationMessage(event) {
|
||||
return (
|
||||
typeof event.data === 'object' &&
|
||||
'nonce' in event.data &&
|
||||
'payload' in event.data
|
||||
)
|
||||
if (typeof event.data === 'object' && typeof event.data.payload === 'object') {
|
||||
const keys = Object.keys(event.data.payload)
|
||||
return (
|
||||
keys.length > 0 &&
|
||||
keys.every((key) => key === 'nonce' || key === 'payload')
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +50,12 @@
|
||||
* @return {boolean} - if the data is able to transform into an isolation payload
|
||||
*/
|
||||
function isIsolationPayload(data) {
|
||||
return typeof data === 'object' && 'callback' in data && 'error' in data
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
'callback' in data &&
|
||||
'error' in data &&
|
||||
!isIsolationMessage(data)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
31
core/tauri/scripts/process-ipc-message-fn.js
Normal file
31
core/tauri/scripts/process-ipc-message-fn.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// this is a function and not an iife so use it carefully
|
||||
|
||||
(function (message) {
|
||||
if (message instanceof ArrayBuffer || ArrayBuffer.isView(message) || Array.isArray(message)) {
|
||||
return {
|
||||
contentType: 'application/octet-stream',
|
||||
data: message
|
||||
}
|
||||
} else {
|
||||
const data = JSON.stringify(message, (_k, val) => {
|
||||
if (val instanceof Map) {
|
||||
let o = {};
|
||||
val.forEach((v, k) => o[k] = v);
|
||||
return o;
|
||||
} else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
|
||||
return `__CHANNEL__:${val.id}`
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
contentType: 'application/json',
|
||||
data
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
(function (message) {
|
||||
return JSON.stringify(message, (_k, val) => {
|
||||
if (val instanceof Map) {
|
||||
let o = {};
|
||||
val.forEach((v, k) => o[k] = v);
|
||||
return o;
|
||||
} else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') {
|
||||
return `__CHANNEL__:${val.id}`
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
//! The Tauri API interface.
|
||||
|
||||
pub mod ipc;
|
||||
|
||||
mod error;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
pub(crate) mod tray;
|
||||
|
||||
use crate::{
|
||||
api::ipc::CallbackFn,
|
||||
command::{CommandArg, CommandItem},
|
||||
hooks::{
|
||||
window_invoke_responder, InvokeHandler, InvokeResponder, OnPageLoad, PageLoadPayload, SetupHook,
|
||||
ipc::{
|
||||
channel::ChannelDataIpcQueue, CallbackFn, Invoke, InvokeError, InvokeHandler, InvokeResponder,
|
||||
InvokeResponse,
|
||||
},
|
||||
manager::{Asset, CustomProtocol, WindowManager},
|
||||
plugin::{Plugin, PluginStore},
|
||||
@@ -23,14 +23,16 @@ use crate::{
|
||||
sealed::{ManagerBase, RuntimeOrDispatch},
|
||||
utils::config::Config,
|
||||
utils::{assets::Assets, Env},
|
||||
Context, DeviceEventFilter, EventLoopMessage, Icon, Invoke, InvokeError, InvokeResponse, Manager,
|
||||
Monitor, Runtime, Scopes, StateManager, Theme, Window,
|
||||
Context, DeviceEventFilter, EventLoopMessage, Icon, Manager, Monitor, Runtime, Scopes,
|
||||
StateManager, Theme, Window,
|
||||
};
|
||||
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
use crate::scope::FsScope;
|
||||
|
||||
use raw_window_handle::HasRawDisplayHandle;
|
||||
use serde::Deserialize;
|
||||
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
|
||||
use tauri_macros::default_runtime;
|
||||
use tauri_runtime::window::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
@@ -55,6 +57,24 @@ pub(crate) type GlobalMenuEventListener<R> = Box<dyn Fn(WindowMenuEvent<R>) + Se
|
||||
pub(crate) type GlobalWindowEventListener<R> = Box<dyn Fn(GlobalWindowEvent<R>) + Send + Sync>;
|
||||
#[cfg(all(desktop, feature = "system-tray"))]
|
||||
type SystemTrayEventListener<R> = Box<dyn Fn(&AppHandle<R>, tray::SystemTrayEvent) + Send + Sync>;
|
||||
/// A closure that is run when the Tauri application is setting up.
|
||||
pub type SetupHook<R> =
|
||||
Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
|
||||
/// A closure that is run once every time a window is created and loaded.
|
||||
pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static;
|
||||
|
||||
/// The payload for the [`OnPageLoad`] hook.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PageLoadPayload {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl PageLoadPayload {
|
||||
/// The page URL.
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
/// Api exposed on the `ExitRequested` event.
|
||||
#[derive(Debug)]
|
||||
@@ -799,7 +819,7 @@ pub struct Builder<R: Runtime> {
|
||||
invoke_handler: Box<InvokeHandler<R>>,
|
||||
|
||||
/// The JS message responder.
|
||||
pub(crate) invoke_responder: Arc<InvokeResponder<R>>,
|
||||
invoke_responder: Option<Arc<InvokeResponder<R>>>,
|
||||
|
||||
/// The script that initializes the `window.__TAURI_POST_MESSAGE__` function.
|
||||
invoke_initialization_script: String,
|
||||
@@ -847,6 +867,17 @@ pub struct Builder<R: Runtime> {
|
||||
device_event_filter: DeviceEventFilter,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/ipc-protocol.js")]
|
||||
struct InvokeInitializationScript<'a> {
|
||||
/// The function that processes the IPC message.
|
||||
#[raw]
|
||||
process_ipc_message_fn: &'a str,
|
||||
os_name: &'a str,
|
||||
fetch_channel_data_command: &'a str,
|
||||
use_custom_protocol: bool,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Builder<R> {
|
||||
/// Creates a new App builder.
|
||||
pub fn new() -> Self {
|
||||
@@ -855,9 +886,16 @@ impl<R: Runtime> Builder<R> {
|
||||
runtime_any_thread: false,
|
||||
setup: Box::new(|_| Ok(())),
|
||||
invoke_handler: Box::new(|_| false),
|
||||
invoke_responder: Arc::new(window_invoke_responder),
|
||||
invoke_initialization_script:
|
||||
format!("Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {{ value: (message) => window.ipc.postMessage({}(message)) }})", crate::manager::STRINGIFY_IPC_MESSAGE_FN),
|
||||
invoke_responder: None,
|
||||
invoke_initialization_script: InvokeInitializationScript {
|
||||
process_ipc_message_fn: crate::manager::PROCESS_IPC_MESSAGE_FN,
|
||||
os_name: std::env::consts::OS,
|
||||
fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
|
||||
use_custom_protocol: cfg!(ipc_custom_protocol),
|
||||
}
|
||||
.render_default(&Default::default())
|
||||
.unwrap()
|
||||
.into_string(),
|
||||
on_page_load: Box::new(|_, _| ()),
|
||||
pending_windows: Default::default(),
|
||||
plugins: PluginStore::default(),
|
||||
@@ -916,14 +954,14 @@ impl<R: Runtime> Builder<R> {
|
||||
/// The `responder` is a function that will be called when a command has been executed and must send a response to the JS layer.
|
||||
///
|
||||
/// The `initialization_script` is a script that initializes `window.__TAURI_POST_MESSAGE__`.
|
||||
/// That function must take the `message: object` argument and send it to the backend.
|
||||
/// That function must take the `(message: object, options: object)` arguments and send it to the backend.
|
||||
#[must_use]
|
||||
pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
|
||||
where
|
||||
F: Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
|
||||
F: Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
|
||||
{
|
||||
self.invoke_initialization_script = initialization_script;
|
||||
self.invoke_responder = Arc::new(responder);
|
||||
self.invoke_responder.replace(Arc::new(responder));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1368,6 +1406,9 @@ impl<R: Runtime> Builder<R> {
|
||||
asset_protocol: FsScope::for_fs_api(&app, &app.config().tauri.security.asset_protocol.scope)?,
|
||||
});
|
||||
|
||||
app.manage(ChannelDataIpcQueue::default());
|
||||
app.handle.plugin(crate::ipc::channel::plugin())?;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let crate::utils::config::WebviewInstallMode::FixedRuntime { path } = &app
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html)
|
||||
//! attribute macro along the way and used by [`crate::generate_handler`] macro.
|
||||
|
||||
use crate::hooks::InvokeError;
|
||||
use crate::InvokeMessage;
|
||||
use crate::Runtime;
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use crate::{
|
||||
ipc::{InvokeBody, InvokeError, InvokeMessage},
|
||||
Runtime,
|
||||
};
|
||||
use serde::{
|
||||
de::{Error, Visitor},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
|
||||
/// Represents a custom command.
|
||||
pub struct CommandItem<'a, R: Runtime> {
|
||||
@@ -62,8 +65,6 @@ impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D {
|
||||
macro_rules! pass {
|
||||
($fn:ident, $($arg:ident: $argt:ty),+) => {
|
||||
fn $fn<V: Visitor<'de>>(self, $($arg: $argt),*) -> Result<V::Value, Self::Error> {
|
||||
use serde::de::Error;
|
||||
|
||||
if self.key.is_empty() {
|
||||
return Err(serde_json::Error::custom(format!(
|
||||
"command {} has an argument with no name with a non-optional value",
|
||||
@@ -71,14 +72,24 @@ macro_rules! pass {
|
||||
)))
|
||||
}
|
||||
|
||||
match self.message.payload.get(self.key) {
|
||||
Some(value) => value.$fn($($arg),*),
|
||||
None => {
|
||||
match &self.message.payload {
|
||||
InvokeBody::Raw(_body) => {
|
||||
Err(serde_json::Error::custom(format!(
|
||||
"command {} missing required key {}",
|
||||
"command {} expected a value for key {} but the IPC call used a bytes payload",
|
||||
self.name, self.key
|
||||
)))
|
||||
}
|
||||
InvokeBody::Json(v) => {
|
||||
match v.get(self.key) {
|
||||
Some(value) => value.$fn($($arg),*),
|
||||
None => {
|
||||
Err(serde_json::Error::custom(format!(
|
||||
"command {} missing required key {}",
|
||||
self.name, self.key
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,9 +122,15 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
|
||||
pass!(deserialize_byte_buf, visitor: V);
|
||||
|
||||
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
|
||||
match self.message.payload.get(self.key) {
|
||||
Some(value) => value.deserialize_option(visitor),
|
||||
None => visitor.visit_none(),
|
||||
match &self.message.payload {
|
||||
InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!(
|
||||
"command {} expected a value for key {} but the IPC call used a bytes payload",
|
||||
self.name, self.key
|
||||
))),
|
||||
InvokeBody::Json(v) => match v.get(self.key) {
|
||||
Some(value) => value.deserialize_option(visitor),
|
||||
None => visitor.visit_none(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,46 +172,47 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
|
||||
/// Nothing in this module is considered stable.
|
||||
#[doc(hidden)]
|
||||
pub mod private {
|
||||
use crate::{InvokeError, InvokeResolver, Runtime};
|
||||
use crate::{
|
||||
ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse},
|
||||
Runtime,
|
||||
};
|
||||
use futures_util::{FutureExt, TryFutureExt};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
|
||||
// ===== impl Serialize =====
|
||||
// ===== impl IpcResponse =====
|
||||
|
||||
pub struct SerializeTag;
|
||||
pub struct ResponseTag;
|
||||
|
||||
pub trait SerializeKind {
|
||||
pub trait ResponseKind {
|
||||
#[inline(always)]
|
||||
fn blocking_kind(&self) -> SerializeTag {
|
||||
SerializeTag
|
||||
fn blocking_kind(&self) -> ResponseTag {
|
||||
ResponseTag
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn async_kind(&self) -> SerializeTag {
|
||||
SerializeTag
|
||||
fn async_kind(&self) -> ResponseTag {
|
||||
ResponseTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> SerializeKind for &T {}
|
||||
impl<T: IpcResponse> ResponseKind for &T {}
|
||||
|
||||
impl SerializeTag {
|
||||
impl ResponseTag {
|
||||
#[inline(always)]
|
||||
pub fn block<R, T>(self, value: T, resolver: InvokeResolver<R>)
|
||||
where
|
||||
R: Runtime,
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
{
|
||||
resolver.respond(Ok(value))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn future<T>(self, value: T) -> impl Future<Output = Result<Value, InvokeError>>
|
||||
pub fn future<T>(self, value: T) -> impl Future<Output = Result<InvokeBody, InvokeError>>
|
||||
where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
{
|
||||
std::future::ready(serde_json::to_value(value).map_err(InvokeError::from_serde_json))
|
||||
std::future::ready(value.body().map_err(InvokeError::from_error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,14 +232,14 @@ pub mod private {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, E: Into<InvokeError>> ResultKind for Result<T, E> {}
|
||||
impl<T: IpcResponse, E: Into<InvokeError>> ResultKind for Result<T, E> {}
|
||||
|
||||
impl ResultTag {
|
||||
#[inline(always)]
|
||||
pub fn block<R, T, E>(self, value: Result<T, E>, resolver: InvokeResolver<R>)
|
||||
where
|
||||
R: Runtime,
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
{
|
||||
resolver.respond(value.map_err(Into::into))
|
||||
@@ -231,20 +249,20 @@ pub mod private {
|
||||
pub fn future<T, E>(
|
||||
self,
|
||||
value: Result<T, E>,
|
||||
) -> impl Future<Output = Result<Value, InvokeError>>
|
||||
) -> impl Future<Output = Result<InvokeBody, InvokeError>>
|
||||
where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
{
|
||||
std::future::ready(
|
||||
value
|
||||
.map_err(Into::into)
|
||||
.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)),
|
||||
.and_then(|value| value.body().map_err(InvokeError::from_error)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Future<Output = impl Serialize> =====
|
||||
// ===== Future<Output = impl IpcResponse> =====
|
||||
|
||||
pub struct FutureTag;
|
||||
|
||||
@@ -254,16 +272,16 @@ pub mod private {
|
||||
FutureTag
|
||||
}
|
||||
}
|
||||
impl<T: Serialize, F: Future<Output = T>> FutureKind for &F {}
|
||||
impl<T: IpcResponse, F: Future<Output = T>> FutureKind for &F {}
|
||||
|
||||
impl FutureTag {
|
||||
#[inline(always)]
|
||||
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
|
||||
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
|
||||
where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
F: Future<Output = T> + Send + 'static,
|
||||
{
|
||||
value.map(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
|
||||
value.map(|value| value.body().map_err(InvokeError::from_error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,19 +296,22 @@ pub mod private {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind for F {}
|
||||
impl<T: IpcResponse, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind
|
||||
for F
|
||||
{
|
||||
}
|
||||
|
||||
impl ResultFutureTag {
|
||||
#[inline(always)]
|
||||
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
|
||||
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
|
||||
where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
E: Into<InvokeError>,
|
||||
F: Future<Output = Result<T, E>> + Send,
|
||||
{
|
||||
value.err_into().map(|result| {
|
||||
result.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
|
||||
})
|
||||
value
|
||||
.err_into()
|
||||
.map(|result| result.and_then(|value| value.body().map_err(InvokeError::from_error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{api::ipc::CallbackFn, command, Manager, Result, Runtime, Window};
|
||||
use crate::{command, ipc::CallbackFn, Manager, Result, Runtime, Window};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::Value as JsonValue;
|
||||
use tauri_runtime::window::is_label_valid;
|
||||
|
||||
@@ -9,7 +9,7 @@ use swift_rs::{swift, SRString, SwiftArg};
|
||||
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
os::raw::{c_char, c_int},
|
||||
os::raw::{c_char, c_int, c_ulonglong},
|
||||
};
|
||||
|
||||
type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
|
||||
@@ -23,20 +23,24 @@ impl<'a> SwiftArg<'a> for PluginMessageCallback {
|
||||
}
|
||||
}
|
||||
|
||||
swift!(pub fn post_ipc_message(
|
||||
webview: *const c_void,
|
||||
name: &SRString,
|
||||
method: &SRString,
|
||||
data: *const c_void,
|
||||
callback: usize,
|
||||
error: usize
|
||||
));
|
||||
swift!(pub fn run_plugin_method(
|
||||
type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char);
|
||||
pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn);
|
||||
|
||||
impl<'a> SwiftArg<'a> for ChannelSendDataCallback {
|
||||
type ArgType = ChannelSendDataCallbackFn;
|
||||
|
||||
unsafe fn as_arg(&'a self) -> Self::ArgType {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
swift!(pub fn run_plugin_command(
|
||||
id: i32,
|
||||
name: &SRString,
|
||||
method: &SRString,
|
||||
data: *const c_void,
|
||||
callback: PluginMessageCallback
|
||||
callback: PluginMessageCallback,
|
||||
send_channel_data_callback: ChannelSendDataCallback
|
||||
));
|
||||
swift!(pub fn register_plugin(
|
||||
name: &SRString,
|
||||
@@ -108,7 +112,7 @@ unsafe fn add_json_value_to_array(array: id, value: &JsonValue) {
|
||||
let () = msg_send![array, addObject: number];
|
||||
}
|
||||
JsonValue::String(val) => {
|
||||
let () = msg_send![array, addObject: NSString::new(&val)];
|
||||
let () = msg_send![array, addObject: NSString::new(val)];
|
||||
}
|
||||
JsonValue::Array(val) => {
|
||||
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
|
||||
@@ -130,7 +134,7 @@ unsafe fn add_json_value_to_array(array: id, value: &JsonValue) {
|
||||
}
|
||||
|
||||
unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) {
|
||||
let key = NSString::new(&key);
|
||||
let key = NSString::new(key);
|
||||
match value {
|
||||
JsonValue::Null => {
|
||||
let null: id = msg_send![class!(NSNull), null];
|
||||
@@ -154,7 +158,7 @@ unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) {
|
||||
let () = msg_send![data, setObject:number forKey: key];
|
||||
}
|
||||
JsonValue::String(val) => {
|
||||
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
|
||||
let () = msg_send![data, setObject:NSString::new(val) forKey: key];
|
||||
}
|
||||
JsonValue::Array(val) => {
|
||||
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
|
||||
|
||||
151
core/tauri/src/ipc/channel.rs
Normal file
151
core/tauri/src/ipc/channel.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
command,
|
||||
command::{CommandArg, CommandItem},
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Manager, Runtime, State, Window,
|
||||
};
|
||||
|
||||
use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response};
|
||||
|
||||
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
|
||||
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
|
||||
// TODO: ideally this const references CHANNEL_PLUGIN_NAME
|
||||
pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch";
|
||||
pub(crate) const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id";
|
||||
|
||||
/// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeBody>>>);
|
||||
|
||||
/// An IPC channel.
|
||||
#[derive(Clone)]
|
||||
pub struct Channel {
|
||||
id: u32,
|
||||
on_message: Arc<dyn Fn(InvokeBody) -> crate::Result<()> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Serialize for Channel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
/// Creates a new channel with the given message handler.
|
||||
pub fn new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
|
||||
on_message: F,
|
||||
) -> Self {
|
||||
Self::_new(rand::random(), on_message)
|
||||
}
|
||||
|
||||
pub(crate) fn _new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
|
||||
id: u32,
|
||||
on_message: F,
|
||||
) -> Self {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let channel = Self {
|
||||
id,
|
||||
on_message: Arc::new(on_message),
|
||||
};
|
||||
|
||||
#[cfg(mobile)]
|
||||
crate::plugin::mobile::register_channel(channel.clone());
|
||||
|
||||
channel
|
||||
}
|
||||
|
||||
pub(crate) fn from_ipc<R: Runtime>(window: Window<R>, callback: CallbackFn) -> Self {
|
||||
Channel::_new(callback.0, move |body| {
|
||||
let data_id = rand::random();
|
||||
window
|
||||
.state::<ChannelDataIpcQueue>()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(data_id, body);
|
||||
window.eval(&format!(
|
||||
"__TAURI_INVOKE__('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': {data_id} }} }}).then(window['_' + {}]).catch(console.error)",
|
||||
callback.0
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn load_from_ipc<R: Runtime>(
|
||||
window: Window<R>,
|
||||
value: impl AsRef<str>,
|
||||
) -> Option<Self> {
|
||||
value
|
||||
.as_ref()
|
||||
.split_once(IPC_PAYLOAD_PREFIX)
|
||||
.and_then(|(_prefix, id)| id.parse().ok())
|
||||
.map(|callback_id| Self::from_ipc(window, CallbackFn(callback_id)))
|
||||
}
|
||||
|
||||
/// The channel identifier.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Sends the given data through the channel.
|
||||
pub fn send<T: IpcResponse>(&self, data: T) -> crate::Result<()> {
|
||||
let body = data.body()?;
|
||||
(self.on_message)(body)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, R: Runtime> CommandArg<'de, R> for Channel {
|
||||
/// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
|
||||
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
|
||||
let name = command.name;
|
||||
let arg = command.key;
|
||||
let window = command.message.window();
|
||||
let value: String =
|
||||
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
|
||||
Channel::load_from_ipc(window, &value).ok_or_else(|| {
|
||||
InvokeError::from_anyhow(anyhow::anyhow!(
|
||||
"invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
fn fetch(
|
||||
request: Request<'_>,
|
||||
cache: State<'_, ChannelDataIpcQueue>,
|
||||
) -> Result<Response, &'static str> {
|
||||
if let Some(id) = request
|
||||
.headers()
|
||||
.get(CHANNEL_ID_HEADER_NAME)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|id| id.parse().ok())
|
||||
{
|
||||
if let Some(data) = cache.0.lock().unwrap().remove(&id) {
|
||||
Ok(Response::new(data))
|
||||
} else {
|
||||
Err("data not found")
|
||||
}
|
||||
} else {
|
||||
Err("missing channel id header")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
|
||||
PluginBuilder::new(CHANNEL_PLUGIN_NAME)
|
||||
.invoke_handler(crate::generate_handler![fetch])
|
||||
.build()
|
||||
}
|
||||
@@ -2,82 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Types and functions related to Inter Procedure Call(IPC).
|
||||
//!
|
||||
//! This module includes utilities to send messages to the JS layer of the webview.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use serde_json::value::RawValue;
|
||||
pub use serialize_to_javascript::Options as SerializeOptions;
|
||||
use serialize_to_javascript::Serialized;
|
||||
use tauri_macros::default_runtime;
|
||||
|
||||
use crate::{
|
||||
command::{CommandArg, CommandItem},
|
||||
InvokeError, Runtime, Window,
|
||||
};
|
||||
|
||||
const CHANNEL_PREFIX: &str = "__CHANNEL__:";
|
||||
|
||||
/// An IPC channel.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
pub struct Channel<R: Runtime> {
|
||||
id: CallbackFn,
|
||||
window: Window<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clone for Channel<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
id: self.id,
|
||||
window: self.window.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Channel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{CHANNEL_PREFIX}{}", self.id.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Channel<R> {
|
||||
/// Sends the given data through the channel.
|
||||
pub fn send<S: Serialize>(&self, data: &S) -> crate::Result<()> {
|
||||
let js = format_callback(self.id, data)?;
|
||||
self.window.eval(&js)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, R: Runtime> CommandArg<'de, R> for Channel<R> {
|
||||
/// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`].
|
||||
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
|
||||
let name = command.name;
|
||||
let arg = command.key;
|
||||
let window = command.message.window();
|
||||
let value: String =
|
||||
Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?;
|
||||
if let Some(callback_id) = value
|
||||
.split_once(CHANNEL_PREFIX)
|
||||
.and_then(|(_prefix, id)| id.parse().ok())
|
||||
{
|
||||
return Ok(Channel {
|
||||
id: CallbackFn(callback_id),
|
||||
window,
|
||||
});
|
||||
}
|
||||
Err(InvokeError::from_anyhow(anyhow::anyhow!(
|
||||
"invalid channel value `{value}`, expected a string in the `{CHANNEL_PREFIX}ID` format"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct CallbackFn(pub usize);
|
||||
use super::CallbackFn;
|
||||
|
||||
/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490).
|
||||
///
|
||||
@@ -111,22 +40,9 @@ const MIN_JSON_PARSE_LEN: usize = 10_240;
|
||||
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
|
||||
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
|
||||
/// character to end a string that was opened with it.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tauri::api::ipc::{serialize_js_with, SerializeOptions};
|
||||
/// #[derive(serde::Serialize)]
|
||||
/// struct Foo {
|
||||
/// bar: String,
|
||||
/// }
|
||||
/// let foo = Foo { bar: "x".repeat(20_000).into() };
|
||||
/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({v})")).unwrap();
|
||||
/// assert_eq!(value, format!("console.log(JSON.parse('{{\"bar\":\"{}\"}}'))", foo.bar));
|
||||
/// ```
|
||||
pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
|
||||
fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
|
||||
value: &T,
|
||||
options: SerializeOptions,
|
||||
options: serialize_to_javascript::Options,
|
||||
cb: F,
|
||||
) -> crate::api::Result<String> {
|
||||
// get a raw &str representation of a serialized json value.
|
||||
@@ -161,80 +77,13 @@ pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
|
||||
Ok(return_val)
|
||||
}
|
||||
|
||||
/// Transforms & escapes a JSON value.
|
||||
///
|
||||
/// This is a convenience function for [`serialize_js_with`], simply allocating the result to a String.
|
||||
///
|
||||
/// For usage in functions where performance is more important than code readability, see [`serialize_js_with`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust,no_run
|
||||
/// use tauri::{Manager, api::ipc::serialize_js};
|
||||
/// use serde::Serialize;
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct Foo {
|
||||
/// bar: String,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct Bar {
|
||||
/// baz: u32,
|
||||
/// }
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let window = app.get_window("main").unwrap();
|
||||
/// window.eval(&format!(
|
||||
/// "console.log({}, {})",
|
||||
/// serialize_js(&Foo { bar: "bar".to_string() }).unwrap(),
|
||||
/// serialize_js(&Bar { baz: 0 }).unwrap()),
|
||||
/// )?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
|
||||
serialize_js_with(value, Default::default(), |v| v.into())
|
||||
}
|
||||
|
||||
/// Formats a function name and argument to be evaluated as callback.
|
||||
///
|
||||
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
|
||||
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
|
||||
/// than 10 KiB with `JSON.parse('...')`.
|
||||
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
|
||||
///
|
||||
/// # Examples
|
||||
/// - With string literals:
|
||||
/// ```
|
||||
/// use tauri::api::ipc::{CallbackFn, format_callback};
|
||||
/// // callback with a string argument
|
||||
/// let cb = format_callback(CallbackFn(12345), &"the string response").unwrap();
|
||||
/// assert!(cb.contains(r#"window["_12345"]("the string response")"#));
|
||||
/// ```
|
||||
///
|
||||
/// - With types implement [`serde::Serialize`]:
|
||||
/// ```
|
||||
/// use tauri::api::ipc::{CallbackFn, format_callback};
|
||||
/// use serde::Serialize;
|
||||
///
|
||||
/// // callback with large JSON argument
|
||||
/// #[derive(Serialize)]
|
||||
/// struct MyResponse {
|
||||
/// value: String
|
||||
/// }
|
||||
///
|
||||
/// let cb = format_callback(
|
||||
/// CallbackFn(6789),
|
||||
/// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap()
|
||||
/// }).expect("failed to serialize");
|
||||
///
|
||||
/// assert!(cb.contains(r#"window["_6789"](JSON.parse('{"value":"XXXXXXXXX"#));
|
||||
/// ```
|
||||
pub fn format_callback<T: Serialize>(
|
||||
function_name: CallbackFn,
|
||||
arg: &T,
|
||||
) -> crate::api::Result<String> {
|
||||
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::api::Result<String> {
|
||||
serialize_js_with(arg, Default::default(), |arg| {
|
||||
format!(
|
||||
r#"
|
||||
@@ -258,42 +107,33 @@ pub fn format_callback<T: Serialize>(
|
||||
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
|
||||
///
|
||||
/// Note that the callback strings are automatically generated by the `invoke` helper.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use tauri::api::ipc::{CallbackFn, format_callback_result};
|
||||
/// let res: Result<u8, &str> = Ok(5);
|
||||
/// let cb = format_callback_result(res, CallbackFn(145), CallbackFn(0)).expect("failed to format");
|
||||
/// assert!(cb.contains(r#"window["_145"](5)"#));
|
||||
///
|
||||
/// let res: Result<&str, &str> = Err("error message here");
|
||||
/// let cb = format_callback_result(res, CallbackFn(2), CallbackFn(1)).expect("failed to format");
|
||||
/// assert!(cb.contains(r#"window["_1"]("error message here")"#));
|
||||
/// ```
|
||||
// TODO: better example to explain
|
||||
pub fn format_callback_result<T: Serialize, E: Serialize>(
|
||||
pub fn format_result<T: Serialize, E: Serialize>(
|
||||
result: Result<T, E>,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) -> crate::api::Result<String> {
|
||||
match result {
|
||||
Ok(res) => format_callback(success_callback, &res),
|
||||
Err(err) => format_callback(error_callback, &err),
|
||||
Ok(res) => format(success_callback, &res),
|
||||
Err(err) => format(error_callback, &err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::api::ipc::*;
|
||||
use super::*;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
use quickcheck_macros::quickcheck;
|
||||
|
||||
impl Arbitrary for CallbackFn {
|
||||
fn arbitrary(g: &mut Gen) -> CallbackFn {
|
||||
CallbackFn(usize::arbitrary(g))
|
||||
CallbackFn(u32::arbitrary(g))
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_js<T: Serialize>(value: &T) -> crate::api::Result<String> {
|
||||
serialize_js_with(value, Default::default(), |v| v.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_js() {
|
||||
assert_eq!(serialize_js(&()).unwrap(), "null");
|
||||
@@ -346,7 +186,7 @@ mod test {
|
||||
#[quickcheck]
|
||||
fn qc_formatting(f: CallbackFn, a: String) -> bool {
|
||||
// call format callback
|
||||
let fc = format_callback(f, &a).unwrap();
|
||||
let fc = format(f, &a).unwrap();
|
||||
fc.contains(&format!(
|
||||
r#"window["_{}"](JSON.parse('{}'))"#,
|
||||
f.0,
|
||||
@@ -358,11 +198,10 @@ mod test {
|
||||
))
|
||||
}
|
||||
|
||||
// check arbitrary strings in format_callback_result
|
||||
// check arbitrary strings in format_result
|
||||
#[quickcheck]
|
||||
fn qc_format_res(result: Result<String, String>, c: CallbackFn, ec: CallbackFn) -> bool {
|
||||
let resp =
|
||||
format_callback_result(result.clone(), c, ec).expect("failed to format callback result");
|
||||
let resp = format_result(result.clone(), c, ec).expect("failed to format callback result");
|
||||
let (function, value) = match result {
|
||||
Ok(v) => (c, v),
|
||||
Err(e) => (ec, e),
|
||||
@@ -2,77 +2,156 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
api::ipc::{format_callback, format_callback_result, CallbackFn},
|
||||
app::App,
|
||||
Runtime, StateManager, Window,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serialize_to_javascript::{default_template, Template};
|
||||
use std::{future::Future, sync::Arc};
|
||||
//! Types and functions related to Inter Procedure Call(IPC).
|
||||
//!
|
||||
//! This module includes utilities to send messages to the JS layer of the webview.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::Future;
|
||||
use http::HeaderMap;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
pub use serialize_to_javascript::Options as SerializeOptions;
|
||||
use tauri_macros::default_runtime;
|
||||
|
||||
/// A closure that is run when the Tauri application is setting up.
|
||||
pub type SetupHook<R> =
|
||||
Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
|
||||
use crate::{
|
||||
command::{CommandArg, CommandItem},
|
||||
Runtime, StateManager, Window,
|
||||
};
|
||||
|
||||
pub(crate) mod channel;
|
||||
#[cfg(not(ipc_custom_protocol))]
|
||||
pub(crate) mod format_callback;
|
||||
pub(crate) mod protocol;
|
||||
|
||||
pub use channel::Channel;
|
||||
|
||||
/// A closure that is run every time Tauri receives a message it doesn't explicitly handle.
|
||||
pub type InvokeHandler<R> = dyn Fn(Invoke<R>) -> bool + Send + Sync + 'static;
|
||||
|
||||
/// A closure that is responsible for respond a JS message.
|
||||
pub type InvokeResponder<R> =
|
||||
dyn Fn(Window<R>, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
|
||||
dyn Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
|
||||
type OwnedInvokeResponder<R> =
|
||||
dyn Fn(Window<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
|
||||
|
||||
/// A closure that is run once every time a window is created and loaded.
|
||||
pub type OnPageLoad<R> = dyn Fn(Window<R>, PageLoadPayload) + Send + Sync + 'static;
|
||||
|
||||
// todo: why is this derive broken but the output works manually?
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/ipc.js")]
|
||||
pub(crate) struct IpcJavascript<'a> {
|
||||
pub(crate) isolation_origin: &'a str,
|
||||
/// Possible values of an IPC payload.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InvokeBody {
|
||||
/// Json payload.
|
||||
Json(JsonValue),
|
||||
/// Bytes payload.
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/isolation.js")]
|
||||
pub(crate) struct IsolationJavascript<'a> {
|
||||
pub(crate) isolation_src: &'a str,
|
||||
pub(crate) style: &'a str,
|
||||
}
|
||||
|
||||
/// The payload for the [`OnPageLoad`] hook.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PageLoadPayload {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl PageLoadPayload {
|
||||
/// The page URL.
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
impl Default for InvokeBody {
|
||||
fn default() -> Self {
|
||||
Self::Json(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// The payload used on the IPC invoke.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InvokePayload {
|
||||
/// The invoke command.
|
||||
pub cmd: String,
|
||||
/// The success callback.
|
||||
pub callback: CallbackFn,
|
||||
/// The error callback.
|
||||
pub error: CallbackFn,
|
||||
/// The payload of the message.
|
||||
#[serde(flatten)]
|
||||
pub inner: JsonValue,
|
||||
impl From<JsonValue> for InvokeBody {
|
||||
fn from(value: JsonValue) -> Self {
|
||||
Self::Json(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for InvokeBody {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Raw(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcResponse for InvokeBody {
|
||||
fn body(self) -> crate::Result<InvokeBody> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl InvokeBody {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn into_json(self) -> JsonValue {
|
||||
match self {
|
||||
Self::Json(v) => v,
|
||||
Self::Raw(v) => {
|
||||
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to deserialize the invoke body.
|
||||
pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
|
||||
match self {
|
||||
InvokeBody::Json(v) => serde_json::from_value(v),
|
||||
InvokeBody::Raw(v) => serde_json::from_slice(&v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The IPC request.
|
||||
#[derive(Debug)]
|
||||
pub struct Request<'a> {
|
||||
body: &'a InvokeBody,
|
||||
headers: &'a HeaderMap,
|
||||
}
|
||||
|
||||
impl<'a> Request<'a> {
|
||||
/// The request body.
|
||||
pub fn body(&self) -> &InvokeBody {
|
||||
self.body
|
||||
}
|
||||
|
||||
/// Thr request headers.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> {
|
||||
/// Returns the invoke [`Request`].
|
||||
fn from_command(command: CommandItem<'a, R>) -> Result<Self, InvokeError> {
|
||||
Ok(Self {
|
||||
body: command.message.payload(),
|
||||
headers: command.message.headers(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a type as a response to an IPC call.
|
||||
pub trait IpcResponse {
|
||||
/// Resolve the IPC response body.
|
||||
fn body(self) -> crate::Result<InvokeBody>;
|
||||
}
|
||||
|
||||
impl<T: Serialize> IpcResponse for T {
|
||||
fn body(self) -> crate::Result<InvokeBody> {
|
||||
serde_json::to_value(self)
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The IPC request.
|
||||
pub struct Response {
|
||||
body: InvokeBody,
|
||||
}
|
||||
|
||||
impl IpcResponse for Response {
|
||||
fn body(self) -> crate::Result<InvokeBody> {
|
||||
Ok(self.body)
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Defines a response with the given body.
|
||||
pub fn new(body: impl Into<InvokeBody>) -> Self {
|
||||
Self { body: body.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The message and resolver given to a custom command.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
#[derive(Debug)]
|
||||
pub struct Invoke<R: Runtime> {
|
||||
/// The message passed.
|
||||
pub message: InvokeMessage<R>,
|
||||
@@ -83,12 +162,12 @@ pub struct Invoke<R: Runtime> {
|
||||
|
||||
/// Error response from an [`InvokeMessage`].
|
||||
#[derive(Debug)]
|
||||
pub struct InvokeError(JsonValue);
|
||||
pub struct InvokeError(pub JsonValue);
|
||||
|
||||
impl InvokeError {
|
||||
/// Create an [`InvokeError`] as a string of the [`serde_json::Error`] message.
|
||||
/// Create an [`InvokeError`] as a string of the [`std::error::Error`] message.
|
||||
#[inline(always)]
|
||||
pub fn from_serde_json(error: serde_json::Error) -> Self {
|
||||
pub fn from_error<E: std::error::Error>(error: E) -> Self {
|
||||
Self(JsonValue::String(error.to_string()))
|
||||
}
|
||||
|
||||
@@ -104,7 +183,7 @@ impl<T: Serialize> From<T> for InvokeError {
|
||||
fn from(value: T) -> Self {
|
||||
serde_json::to_value(value)
|
||||
.map(Self)
|
||||
.unwrap_or_else(Self::from_serde_json)
|
||||
.unwrap_or_else(Self::from_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,31 +198,20 @@ impl From<crate::Error> for InvokeError {
|
||||
#[derive(Debug)]
|
||||
pub enum InvokeResponse {
|
||||
/// Resolve the promise.
|
||||
Ok(JsonValue),
|
||||
Ok(InvokeBody),
|
||||
/// Reject the promise.
|
||||
Err(InvokeError),
|
||||
}
|
||||
|
||||
impl InvokeResponse {
|
||||
/// Turn a [`InvokeResponse`] back into a serializable result.
|
||||
#[inline(always)]
|
||||
pub fn into_result(self) -> Result<JsonValue, JsonValue> {
|
||||
match self {
|
||||
Self::Ok(v) => Ok(v),
|
||||
Self::Err(e) => Err(e.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> From<Result<T, InvokeError>> for InvokeResponse {
|
||||
impl<T: IpcResponse, E: Into<InvokeError>> From<Result<T, E>> for InvokeResponse {
|
||||
#[inline]
|
||||
fn from(result: Result<T, InvokeError>) -> Self {
|
||||
fn from(result: Result<T, E>) -> Self {
|
||||
match result {
|
||||
Ok(ok) => match serde_json::to_value(ok) {
|
||||
Ok(ok) => match ok.body() {
|
||||
Ok(value) => Self::Ok(value),
|
||||
Err(err) => Self::Err(InvokeError::from_serde_json(err)),
|
||||
Err(err) => Self::Err(InvokeError::from_error(err)),
|
||||
},
|
||||
Err(err) => Self::Err(err),
|
||||
Err(err) => Self::Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,9 +224,10 @@ impl From<InvokeError> for InvokeResponse {
|
||||
|
||||
/// Resolver of a invoke message.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
#[derive(Debug)]
|
||||
pub struct InvokeResolver<R: Runtime> {
|
||||
window: Window<R>,
|
||||
responder: Arc<OwnedInvokeResponder<R>>,
|
||||
cmd: String,
|
||||
pub(crate) callback: CallbackFn,
|
||||
pub(crate) error: CallbackFn,
|
||||
}
|
||||
@@ -167,6 +236,8 @@ impl<R: Runtime> Clone for InvokeResolver<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
window: self.window.clone(),
|
||||
responder: self.responder.clone(),
|
||||
cmd: self.cmd.clone(),
|
||||
callback: self.callback,
|
||||
error: self.error,
|
||||
}
|
||||
@@ -174,9 +245,17 @@ impl<R: Runtime> Clone for InvokeResolver<R> {
|
||||
}
|
||||
|
||||
impl<R: Runtime> InvokeResolver<R> {
|
||||
pub(crate) fn new(window: Window<R>, callback: CallbackFn, error: CallbackFn) -> Self {
|
||||
pub(crate) fn new(
|
||||
window: Window<R>,
|
||||
responder: Arc<OwnedInvokeResponder<R>>,
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
) -> Self {
|
||||
Self {
|
||||
window,
|
||||
responder,
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
}
|
||||
@@ -185,43 +264,67 @@ impl<R: Runtime> InvokeResolver<R> {
|
||||
/// Reply to the invoke promise with an async task.
|
||||
pub fn respond_async<T, F>(self, task: F)
|
||||
where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
|
||||
{
|
||||
crate::async_runtime::spawn(async move {
|
||||
Self::return_task(self.window, task, self.callback, self.error).await;
|
||||
Self::return_task(
|
||||
self.window,
|
||||
self.responder,
|
||||
task,
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with an async task which is already serialized.
|
||||
pub fn respond_async_serialized<F>(self, task: F)
|
||||
where
|
||||
F: Future<Output = Result<JsonValue, InvokeError>> + Send + 'static,
|
||||
F: Future<Output = Result<InvokeBody, InvokeError>> + Send + 'static,
|
||||
{
|
||||
crate::async_runtime::spawn(async move {
|
||||
let response = match task.await {
|
||||
Ok(ok) => InvokeResponse::Ok(ok),
|
||||
Err(err) => InvokeResponse::Err(err),
|
||||
};
|
||||
Self::return_result(self.window, response, self.callback, self.error)
|
||||
Self::return_result(
|
||||
self.window,
|
||||
self.responder,
|
||||
response,
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Reply to the invoke promise with a serializable value.
|
||||
pub fn respond<T: Serialize>(self, value: Result<T, InvokeError>) {
|
||||
Self::return_result(self.window, value.into(), self.callback, self.error)
|
||||
pub fn respond<T: IpcResponse>(self, value: Result<T, InvokeError>) {
|
||||
Self::return_result(
|
||||
self.window,
|
||||
self.responder,
|
||||
value.into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the invoke promise with a value.
|
||||
pub fn resolve<T: Serialize>(self, value: T) {
|
||||
Self::return_result(self.window, Ok(value).into(), self.callback, self.error)
|
||||
pub fn resolve<T: IpcResponse>(self, value: T) {
|
||||
self.respond(Ok(value))
|
||||
}
|
||||
|
||||
/// Reject the invoke promise with a value.
|
||||
pub fn reject<T: Serialize>(self, value: T) {
|
||||
Self::return_result(
|
||||
self.window,
|
||||
Result::<(), _>::Err(value.into()).into(),
|
||||
self.responder,
|
||||
Result::<(), _>::Err(value).into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
@@ -229,7 +332,14 @@ impl<R: Runtime> InvokeResolver<R> {
|
||||
|
||||
/// Reject the invoke promise with an [`InvokeError`].
|
||||
pub fn invoke_error(self, error: InvokeError) {
|
||||
Self::return_result(self.window, error.into(), self.callback, self.error)
|
||||
Self::return_result(
|
||||
self.window,
|
||||
self.responder,
|
||||
error.into(),
|
||||
self.cmd,
|
||||
self.callback,
|
||||
self.error,
|
||||
)
|
||||
}
|
||||
|
||||
/// Asynchronously executes the given task
|
||||
@@ -239,52 +349,56 @@ impl<R: Runtime> InvokeResolver<R> {
|
||||
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
|
||||
pub async fn return_task<T, F>(
|
||||
window: Window<R>,
|
||||
responder: Arc<OwnedInvokeResponder<R>>,
|
||||
task: F,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) where
|
||||
T: Serialize,
|
||||
T: IpcResponse,
|
||||
F: Future<Output = Result<T, InvokeError>> + Send + 'static,
|
||||
{
|
||||
let result = task.await;
|
||||
Self::return_closure(window, || result, success_callback, error_callback)
|
||||
Self::return_closure(
|
||||
window,
|
||||
responder,
|
||||
|| result,
|
||||
cmd,
|
||||
success_callback,
|
||||
error_callback,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn return_closure<T: Serialize, F: FnOnce() -> Result<T, InvokeError>>(
|
||||
pub(crate) fn return_closure<T: IpcResponse, F: FnOnce() -> Result<T, InvokeError>>(
|
||||
window: Window<R>,
|
||||
responder: Arc<OwnedInvokeResponder<R>>,
|
||||
f: F,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) {
|
||||
Self::return_result(window, f().into(), success_callback, error_callback)
|
||||
Self::return_result(
|
||||
window,
|
||||
responder,
|
||||
f().into(),
|
||||
cmd,
|
||||
success_callback,
|
||||
error_callback,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn return_result(
|
||||
window: Window<R>,
|
||||
responder: Arc<OwnedInvokeResponder<R>>,
|
||||
response: InvokeResponse,
|
||||
cmd: String,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) {
|
||||
(window.invoke_responder())(window, response, success_callback, error_callback);
|
||||
(responder)(window, cmd, response, success_callback, error_callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_invoke_responder<R: Runtime>(
|
||||
window: Window<R>,
|
||||
response: InvokeResponse,
|
||||
success_callback: CallbackFn,
|
||||
error_callback: CallbackFn,
|
||||
) {
|
||||
let callback_string =
|
||||
match format_callback_result(response.into_result(), success_callback, error_callback) {
|
||||
Ok(callback_string) => callback_string,
|
||||
Err(e) => format_callback(error_callback, &e.to_string())
|
||||
.expect("unable to serialize response string to json"),
|
||||
};
|
||||
|
||||
let _ = window.eval(&callback_string);
|
||||
}
|
||||
|
||||
/// An invoke message.
|
||||
#[default_runtime(crate::Wry, wry)]
|
||||
#[derive(Debug)]
|
||||
@@ -296,7 +410,9 @@ pub struct InvokeMessage<R: Runtime> {
|
||||
/// The IPC command.
|
||||
pub(crate) command: String,
|
||||
/// The JSON argument passed on the invoke message.
|
||||
pub(crate) payload: JsonValue,
|
||||
pub(crate) payload: InvokeBody,
|
||||
/// The request headers.
|
||||
pub(crate) headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clone for InvokeMessage<R> {
|
||||
@@ -306,6 +422,7 @@ impl<R: Runtime> Clone for InvokeMessage<R> {
|
||||
state: self.state.clone(),
|
||||
command: self.command.clone(),
|
||||
payload: self.payload.clone(),
|
||||
headers: self.headers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,13 +433,15 @@ impl<R: Runtime> InvokeMessage<R> {
|
||||
window: Window<R>,
|
||||
state: Arc<StateManager>,
|
||||
command: String,
|
||||
payload: JsonValue,
|
||||
payload: InvokeBody,
|
||||
headers: HeaderMap,
|
||||
) -> Self {
|
||||
Self {
|
||||
window,
|
||||
state,
|
||||
command,
|
||||
payload,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +465,7 @@ impl<R: Runtime> InvokeMessage<R> {
|
||||
|
||||
/// A reference to the payload the invoke received.
|
||||
#[inline(always)]
|
||||
pub fn payload(&self) -> &JsonValue {
|
||||
pub fn payload(&self) -> &InvokeBody {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
@@ -361,4 +480,14 @@ impl<R: Runtime> InvokeMessage<R> {
|
||||
pub fn state_ref(&self) -> &StateManager {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// The request headers.
|
||||
#[inline(always)]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Callback` type is the return value of the `transformCallback` JavaScript function.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct CallbackFn(pub u32);
|
||||
275
core/tauri/src/ipc/protocol.rs
Normal file
275
core/tauri/src/ipc/protocol.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use http::{
|
||||
header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN},
|
||||
HeaderValue, Method, StatusCode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
manager::WindowManager,
|
||||
runtime::http::{Request as HttpRequest, Response as HttpResponse},
|
||||
window::{InvokeRequest, UriSchemeProtocolHandler},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
use super::{CallbackFn, InvokeBody, InvokeResponse};
|
||||
|
||||
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
|
||||
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
|
||||
|
||||
#[cfg(not(ipc_custom_protocol))]
|
||||
pub fn message_handler<R: Runtime>(
|
||||
manager: WindowManager<R>,
|
||||
) -> crate::runtime::webview::WebviewIpcHandler<crate::EventLoopMessage, R> {
|
||||
Box::new(move |window, request| handle_ipc_message(request, &manager, &window.label))
|
||||
}
|
||||
|
||||
pub fn get<R: Runtime>(manager: WindowManager<R>, label: String) -> UriSchemeProtocolHandler {
|
||||
Box::new(move |request| {
|
||||
let mut response = match *request.method() {
|
||||
Method::POST => {
|
||||
let (mut response, content_type) = match handle_ipc_request(request, &manager, &label) {
|
||||
Ok(data) => match data {
|
||||
InvokeResponse::Ok(InvokeBody::Json(v)) => (
|
||||
HttpResponse::new(serde_json::to_vec(&v)?.into()),
|
||||
mime::APPLICATION_JSON,
|
||||
),
|
||||
InvokeResponse::Ok(InvokeBody::Raw(v)) => {
|
||||
(HttpResponse::new(v.into()), mime::APPLICATION_OCTET_STREAM)
|
||||
}
|
||||
InvokeResponse::Err(e) => {
|
||||
let mut response = HttpResponse::new(serde_json::to_vec(&e.0)?.into());
|
||||
response.set_status(StatusCode::BAD_REQUEST);
|
||||
(response, mime::TEXT_PLAIN)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let mut response = HttpResponse::new(e.as_bytes().to_vec().into());
|
||||
response.set_status(StatusCode::BAD_REQUEST);
|
||||
(response, mime::TEXT_PLAIN)
|
||||
}
|
||||
};
|
||||
|
||||
response.set_mimetype(Some(content_type.essence_str().into()));
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
Method::OPTIONS => {
|
||||
let mut r = HttpResponse::new(Vec::new().into());
|
||||
r.headers_mut().insert(
|
||||
ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
HeaderValue::from_static("Content-Type, Tauri-Callback, Tauri-Error, Tauri-Channel-Id"),
|
||||
);
|
||||
r
|
||||
}
|
||||
|
||||
_ => {
|
||||
let mut r = HttpResponse::new(
|
||||
"only POST and OPTIONS are allowed"
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into(),
|
||||
);
|
||||
r.set_status(StatusCode::METHOD_NOT_ALLOWED);
|
||||
r.set_mimetype(Some(mime::TEXT_PLAIN.essence_str().into()));
|
||||
r
|
||||
}
|
||||
};
|
||||
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(ipc_custom_protocol))]
|
||||
fn handle_ipc_message<R: Runtime>(message: String, manager: &WindowManager<R>, label: &str) {
|
||||
if let Some(window) = manager.get_window(label) {
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
pub(crate) struct HeaderMap(http::HeaderMap);
|
||||
|
||||
impl<'de> Deserialize<'de> for HeaderMap {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = std::collections::HashMap::<String, String>::deserialize(deserializer)?;
|
||||
let mut headers = http::HeaderMap::default();
|
||||
for (key, value) in map {
|
||||
if let (Ok(key), Ok(value)) = (
|
||||
http::HeaderName::from_bytes(key.as_bytes()),
|
||||
http::HeaderValue::from_str(&value),
|
||||
) {
|
||||
headers.insert(key, value);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid header `{key}` `{value}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Self(headers))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RequestOptions {
|
||||
headers: HeaderMap,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Message {
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
#[serde(flatten)]
|
||||
payload: serde_json::Value,
|
||||
options: Option<RequestOptions>,
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut invoke_message: Option<crate::Result<Message>> = None;
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
{
|
||||
#[derive(serde::Deserialize)]
|
||||
struct IsolationMessage<'a> {
|
||||
cmd: String,
|
||||
callback: CallbackFn,
|
||||
error: CallbackFn,
|
||||
#[serde(flatten)]
|
||||
payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>,
|
||||
options: Option<RequestOptions>,
|
||||
}
|
||||
|
||||
if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
|
||||
invoke_message.replace(
|
||||
serde_json::from_str::<IsolationMessage<'_>>(&message)
|
||||
.map_err(Into::into)
|
||||
.and_then(|message| {
|
||||
Ok(Message {
|
||||
cmd: message.cmd,
|
||||
callback: message.callback,
|
||||
error: message.error,
|
||||
payload: serde_json::from_slice(&crypto_keys.decrypt(message.payload)?)?,
|
||||
options: message.options,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match invoke_message
|
||||
.unwrap_or_else(|| serde_json::from_str::<Message>(&message).map_err(Into::into))
|
||||
{
|
||||
Ok(message) => {
|
||||
let _ = window.on_message(InvokeRequest {
|
||||
cmd: message.cmd,
|
||||
callback: message.callback,
|
||||
error: message.error,
|
||||
body: message.payload.into(),
|
||||
headers: message.options.map(|o| o.headers.0).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = window.eval(&format!(
|
||||
r#"console.error({})"#,
|
||||
serde_json::Value::String(e.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_ipc_request<R: Runtime>(
|
||||
request: &HttpRequest,
|
||||
manager: &WindowManager<R>,
|
||||
label: &str,
|
||||
) -> std::result::Result<InvokeResponse, String> {
|
||||
if let Some(window) = manager.get_window(label) {
|
||||
// TODO: consume instead
|
||||
#[allow(unused_mut)]
|
||||
let mut body = request.body().clone();
|
||||
|
||||
let cmd = request
|
||||
.uri()
|
||||
.strip_prefix("ipc://localhost/")
|
||||
.map(|c| c.to_string())
|
||||
// the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
|
||||
// where `$P` is not `localhost/*`
|
||||
// in this case the IPC call is considered invalid
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let cmd = percent_encoding::percent_decode(cmd.as_bytes())
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
|
||||
// the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it
|
||||
#[cfg(all(feature = "isolation", ipc_custom_protocol))]
|
||||
if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
|
||||
body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body)
|
||||
.and_then(|raw| crypto_keys.decrypt(raw))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let callback = CallbackFn(
|
||||
request
|
||||
.headers()
|
||||
.get(TAURI_CALLBACK_HEADER_NAME)
|
||||
.ok_or("missing Tauri-Callback header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Tauri callback header value must be a string")?
|
||||
.parse()
|
||||
.map_err(|_| "Tauri callback header value must be a numeric string")?,
|
||||
);
|
||||
let error = CallbackFn(
|
||||
request
|
||||
.headers()
|
||||
.get(TAURI_ERROR_HEADER_NAME)
|
||||
.ok_or("missing Tauri-Error header")?
|
||||
.to_str()
|
||||
.map_err(|_| "Tauri error header value must be a string")?
|
||||
.parse()
|
||||
.map_err(|_| "Tauri error header value must be a numeric string")?,
|
||||
);
|
||||
|
||||
let content_type = request
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|mime| mime.parse())
|
||||
.unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM))
|
||||
.map_err(|_| "unknown content type")?;
|
||||
let body = if content_type == mime::APPLICATION_OCTET_STREAM {
|
||||
body.into()
|
||||
} else if content_type == mime::APPLICATION_JSON {
|
||||
if cfg!(ipc_custom_protocol) {
|
||||
serde_json::from_slice::<serde_json::Value>(&body)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into()
|
||||
} else {
|
||||
// the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it
|
||||
serde_json::Value::Object(Default::default()).into()
|
||||
}
|
||||
} else {
|
||||
return Err(format!("content type {content_type} is not implemented"));
|
||||
};
|
||||
|
||||
let payload = InvokeRequest {
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
body,
|
||||
headers: request.headers().clone(),
|
||||
};
|
||||
|
||||
let rx = window.on_message(payload);
|
||||
Ok(rx.recv().unwrap())
|
||||
} else {
|
||||
Err("window not found".into())
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fn json_to_java<'a, R: Runtime>(
|
||||
}
|
||||
JsonValue::String(val) => (
|
||||
"Ljava/lang/Object;",
|
||||
JObject::from(env.new_string(&val)?).into(),
|
||||
JObject::from(env.new_string(val)?).into(),
|
||||
),
|
||||
JsonValue::Array(val) => {
|
||||
let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?;
|
||||
@@ -60,7 +60,7 @@ fn json_to_java<'a, R: Runtime>(
|
||||
data,
|
||||
"put",
|
||||
format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
|
||||
&[env.new_string(&key)?.into(), val],
|
||||
&[env.new_string(key)?.into(), val],
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//! - **test**: Enables the [`test`] module exposing unit test helpers.
|
||||
//! - **dox**: Internal feature to generate Rust documentation without linking on Linux.
|
||||
//! - **objc-exception**: Wrap each msg_send! in a @try/@catch and panics if an exception is caught, preventing Objective-C from unwinding into Rust.
|
||||
//! - **linux-protocol-headers**: Enables headers support for custom protocol requests on Linux. Requires webkit2gtk v2.36 or above.
|
||||
//! - **linux-ipc-protocol**: Use custom protocol for faster IPC on Linux. Requires webkit2gtk v2.40 or above.
|
||||
//! - **isolation**: Enables the isolation pattern. Enabled by default if the `tauri > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file.
|
||||
//! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one.
|
||||
//! - **devtools**: Enables the developer tools (Web inspector) and [`Window::open_devtools`]. Enabled by default on debug builds.
|
||||
@@ -81,7 +81,7 @@ pub mod async_runtime;
|
||||
pub mod command;
|
||||
mod error;
|
||||
mod event;
|
||||
mod hooks;
|
||||
pub mod ipc;
|
||||
mod manager;
|
||||
mod pattern;
|
||||
pub mod plugin;
|
||||
@@ -120,23 +120,31 @@ macro_rules! android_binding {
|
||||
handlePluginResponse,
|
||||
[i32, JString, JString],
|
||||
);
|
||||
::tauri::wry::application::android_fn!(
|
||||
app_tauri,
|
||||
plugin,
|
||||
PluginManager,
|
||||
sendChannelData,
|
||||
[i64, JString],
|
||||
);
|
||||
|
||||
// this function is a glue between PluginManager.kt > handlePluginResponse and Rust
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn handlePluginResponse(
|
||||
env: JNIEnv,
|
||||
_: JClass,
|
||||
id: i32,
|
||||
success: JString,
|
||||
error: JString,
|
||||
) {
|
||||
pub fn handlePluginResponse(env: JNIEnv, _: JClass, id: i32, success: JString, error: JString) {
|
||||
::tauri::handle_android_plugin_response(env, id, success, error);
|
||||
}
|
||||
|
||||
// this function is a glue between PluginManager.kt > sendChannelData and Rust
|
||||
#[allow(non_snake_case)]
|
||||
pub fn sendChannelData(env: JNIEnv, _: JClass, id: i64, data: JString) {
|
||||
::tauri::send_channel_data(env, id, data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wry", target_os = "android"))]
|
||||
#[doc(hidden)]
|
||||
pub use plugin::mobile::handle_android_plugin_response;
|
||||
pub use plugin::mobile::{handle_android_plugin_response, send_channel_data};
|
||||
#[cfg(all(feature = "wry", target_os = "android"))]
|
||||
#[doc(hidden)]
|
||||
pub use tauri_runtime_wry::wry;
|
||||
@@ -180,10 +188,6 @@ pub use {
|
||||
App, AppHandle, AssetResolver, Builder, CloseRequestApi, GlobalWindowEvent, RunEvent,
|
||||
WindowEvent,
|
||||
},
|
||||
self::hooks::{
|
||||
Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokePayload, InvokeResolver,
|
||||
InvokeResponder, InvokeResponse, OnPageLoad, PageLoadPayload, SetupHook,
|
||||
},
|
||||
self::manager::Asset,
|
||||
self::runtime::{
|
||||
webview::WebviewAttributes,
|
||||
|
||||
@@ -11,36 +11,32 @@ use std::{
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
|
||||
use url::Url;
|
||||
|
||||
use tauri_macros::default_runtime;
|
||||
use tauri_utils::debug_eprintln;
|
||||
#[cfg(feature = "isolation")]
|
||||
use tauri_utils::pattern::isolation::RawIsolationPayload;
|
||||
use tauri_utils::{
|
||||
assets::{AssetKey, CspHash},
|
||||
config::{Csp, CspDirectiveSources},
|
||||
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
};
|
||||
|
||||
use crate::app::{GlobalMenuEventListener, WindowMenuEvent};
|
||||
use crate::hooks::IpcJavascript;
|
||||
#[cfg(feature = "isolation")]
|
||||
use crate::hooks::IsolationJavascript;
|
||||
use crate::pattern::PatternJavascript;
|
||||
use crate::{
|
||||
app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
|
||||
app::{
|
||||
AppHandle, GlobalMenuEventListener, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad,
|
||||
PageLoadPayload, WindowMenuEvent,
|
||||
},
|
||||
event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
|
||||
hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
|
||||
ipc::{Invoke, InvokeHandler, InvokeResponder},
|
||||
pattern::PatternJavascript,
|
||||
plugin::PluginStore,
|
||||
runtime::{
|
||||
http::{
|
||||
MimeType, Request as HttpRequest, Response as HttpResponse,
|
||||
ResponseBuilder as HttpResponseBuilder,
|
||||
},
|
||||
webview::{WebviewIpcHandler, WindowBuilder},
|
||||
webview::WindowBuilder,
|
||||
window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow},
|
||||
},
|
||||
utils::{
|
||||
@@ -48,7 +44,8 @@ use crate::{
|
||||
config::{AppUrl, Config, WindowUrl},
|
||||
PackageInfo,
|
||||
},
|
||||
Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
|
||||
window::{UriSchemeProtocolHandler, WebResourceRequestHandler},
|
||||
Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window,
|
||||
WindowEvent,
|
||||
};
|
||||
|
||||
@@ -70,8 +67,8 @@ const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
|
||||
const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
|
||||
const MENU_EVENT: &str = "tauri://menu";
|
||||
|
||||
pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
|
||||
include_str!("../scripts/stringify-ipc-message-fn.js");
|
||||
pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
|
||||
include_str!("../scripts/process-ipc-message-fn.js");
|
||||
|
||||
// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
|
||||
// and we do not get a secure context without the custom protocol that proxies to the dev server
|
||||
@@ -79,6 +76,20 @@ pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
|
||||
// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
|
||||
const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/isolation.js")]
|
||||
pub(crate) struct IsolationJavascript<'a> {
|
||||
pub(crate) isolation_src: &'a str,
|
||||
pub(crate) style: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/ipc.js")]
|
||||
pub(crate) struct IpcJavascript<'a> {
|
||||
pub(crate) isolation_origin: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// Spaced and quoted Content-Security-Policy hash values.
|
||||
struct CspHashStrings {
|
||||
@@ -229,7 +240,7 @@ pub struct InnerWindowManager<R: Runtime> {
|
||||
/// Window event listeners to all windows.
|
||||
window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
|
||||
/// Responder for invoke calls.
|
||||
invoke_responder: Arc<InvokeResponder<R>>,
|
||||
invoke_responder: Option<Arc<InvokeResponder<R>>>,
|
||||
/// The script that initializes the invoke system.
|
||||
invoke_initialization_script: String,
|
||||
/// Application pattern.
|
||||
@@ -302,7 +313,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
state: StateManager,
|
||||
window_event_listeners: Vec<GlobalWindowEventListener<R>>,
|
||||
(menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
|
||||
(invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String),
|
||||
(invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
|
||||
) -> Self {
|
||||
// generate a random isolation key at runtime
|
||||
#[cfg(feature = "isolation")]
|
||||
@@ -353,7 +364,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
}
|
||||
|
||||
/// The invoke responder.
|
||||
pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
|
||||
pub(crate) fn invoke_responder(&self) -> Option<Arc<InvokeResponder<R>>> {
|
||||
self.inner.invoke_responder.clone()
|
||||
}
|
||||
|
||||
@@ -506,6 +517,14 @@ impl<R: Runtime> WindowManager<R> {
|
||||
registered_scheme_protocols.push("tauri".into());
|
||||
}
|
||||
|
||||
if !registered_scheme_protocols.contains(&"ipc".into()) {
|
||||
pending.register_uri_scheme_protocol(
|
||||
"ipc",
|
||||
crate::ipc::protocol::get(self.clone(), pending.label.clone()),
|
||||
);
|
||||
registered_scheme_protocols.push("ipc".into());
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
if !registered_scheme_protocols.contains(&"asset".into()) {
|
||||
let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
|
||||
@@ -538,7 +557,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
let asset = String::from_utf8_lossy(asset.as_ref());
|
||||
let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
|
||||
runtime_aes_gcm_key: &aes_gcm_key,
|
||||
stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN,
|
||||
process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN,
|
||||
};
|
||||
match template.render(asset.as_ref(), &Default::default()) {
|
||||
Ok(asset) => HttpResponseBuilder::new()
|
||||
@@ -567,43 +586,6 @@ impl<R: Runtime> WindowManager<R> {
|
||||
Ok(pending)
|
||||
}
|
||||
|
||||
fn prepare_ipc_handler(&self) -> WebviewIpcHandler<EventLoopMessage, R> {
|
||||
let manager = self.clone();
|
||||
Box::new(move |window, #[allow(unused_mut)] mut request| {
|
||||
if let Some(window) = manager.get_window(&window.label) {
|
||||
#[cfg(feature = "isolation")]
|
||||
if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
|
||||
match RawIsolationPayload::try_from(request.as_str())
|
||||
.and_then(|raw| crypto_keys.decrypt(raw))
|
||||
{
|
||||
Ok(json) => request = json,
|
||||
Err(e) => {
|
||||
let error: crate::Error = e.into();
|
||||
let _ = window.eval(&format!(
|
||||
r#"console.error({})"#,
|
||||
JsonValue::String(error.to_string())
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<InvokePayload>(&request) {
|
||||
Ok(message) => {
|
||||
let _ = window.on_message(message);
|
||||
}
|
||||
Err(e) => {
|
||||
let error: crate::Error = e.into();
|
||||
let _ = window.eval(&format!(
|
||||
r#"console.error({})"#,
|
||||
JsonValue::String(error.to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
|
||||
let assets = &self.inner.assets;
|
||||
if path.ends_with('/') {
|
||||
@@ -625,7 +607,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
let asset_response = assets
|
||||
.get(&path.as_str().into())
|
||||
.or_else(|| {
|
||||
eprintln!("Asset `{path}` not found; fallback to {path}.html");
|
||||
debug_eprintln!("Asset `{path}` not found; fallback to {path}.html");
|
||||
let fallback = format!("{}.html", path.as_str()).into();
|
||||
let asset = assets.get(&fallback);
|
||||
asset_path = fallback;
|
||||
@@ -687,15 +669,11 @@ impl<R: Runtime> WindowManager<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn prepare_uri_scheme_protocol(
|
||||
&self,
|
||||
window_origin: &str,
|
||||
web_resource_request_handler: Option<
|
||||
Box<dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync>,
|
||||
>,
|
||||
) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
|
||||
{
|
||||
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
|
||||
) -> UriSchemeProtocolHandler {
|
||||
#[cfg(all(dev, mobile))]
|
||||
let url = {
|
||||
let mut url = self.get_url().as_str().to_string();
|
||||
@@ -740,7 +718,6 @@ impl<R: Runtime> WindowManager<R> {
|
||||
|
||||
#[cfg(all(dev, mobile))]
|
||||
let mut response = {
|
||||
use reqwest::StatusCode;
|
||||
let decoded_path = percent_encoding::percent_decode(path.as_bytes())
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
@@ -762,7 +739,7 @@ impl<R: Runtime> WindowManager<R> {
|
||||
Ok(r) => {
|
||||
let mut response_cache_ = response_cache.lock().unwrap();
|
||||
let mut response = None;
|
||||
if r.status() == StatusCode::NOT_MODIFIED {
|
||||
if r.status() == http::StatusCode::NOT_MODIFIED {
|
||||
response = response_cache_.get(&url);
|
||||
}
|
||||
let response = if let Some(r) = response {
|
||||
@@ -846,6 +823,12 @@ impl<R: Runtime> WindowManager<R> {
|
||||
freeze_prototype: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("../scripts/core.js")]
|
||||
struct CoreJavascript<'a> {
|
||||
os_name: &'a str,
|
||||
}
|
||||
|
||||
let bundle_script = if with_global_tauri {
|
||||
include_str!("../scripts/bundle.global.js")
|
||||
} else {
|
||||
@@ -872,7 +855,11 @@ impl<R: Runtime> WindowManager<R> {
|
||||
"window['_' + window.__TAURI__.transformCallback(cb) ]".into()
|
||||
)
|
||||
),
|
||||
core_script: include_str!("../scripts/core.js"),
|
||||
core_script: &CoreJavascript {
|
||||
os_name: std::env::consts::OS,
|
||||
}
|
||||
.render_default(&Default::default())?
|
||||
.into_string(),
|
||||
event_initialization_script: &self.event_initialization_script(),
|
||||
plugin_initialization_script,
|
||||
freeze_prototype,
|
||||
@@ -923,7 +910,7 @@ mod test {
|
||||
StateManager::new(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
(std::sync::Arc::new(|_, _, _, _| ()), "".into()),
|
||||
(None, "".into()),
|
||||
);
|
||||
|
||||
#[cfg(custom_protocol)]
|
||||
@@ -1089,7 +1076,10 @@ impl<R: Runtime> WindowManager<R> {
|
||||
#[allow(clippy::redundant_clone)]
|
||||
app_handle.clone(),
|
||||
)?;
|
||||
pending.ipc_handler = Some(self.prepare_ipc_handler());
|
||||
#[cfg(not(ipc_custom_protocol))]
|
||||
{
|
||||
pending.ipc_handler = Some(crate::ipc::protocol::message_handler(self.clone()));
|
||||
}
|
||||
|
||||
// in `Windows`, we need to force a data_directory
|
||||
// but we do respect user-specification
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
//! The Tauri plugin extension to expand Tauri functionality.
|
||||
|
||||
use crate::{
|
||||
utils::config::PluginConfig, AppHandle, Invoke, InvokeHandler, PageLoadPayload, RunEvent,
|
||||
Runtime, Window,
|
||||
app::PageLoadPayload,
|
||||
ipc::{Invoke, InvokeHandler},
|
||||
utils::config::PluginConfig,
|
||||
AppHandle, RunEvent, Runtime, Window,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::{PluginApi, PluginHandle};
|
||||
|
||||
use crate::Runtime;
|
||||
use crate::{ipc::Channel, AppHandle, Runtime};
|
||||
#[cfg(target_os = "android")]
|
||||
use crate::{
|
||||
runtime::RuntimeHandle,
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -20,11 +20,13 @@ use std::{
|
||||
sync::{mpsc::channel, Mutex},
|
||||
};
|
||||
|
||||
type PendingPluginCallHandler =
|
||||
Box<dyn FnOnce(Result<serde_json::Value, serde_json::Value>) + Send + 'static>;
|
||||
type PluginResponse = Result<serde_json::Value, serde_json::Value>;
|
||||
|
||||
type PendingPluginCallHandler = Box<dyn FnOnce(PluginResponse) + Send + 'static>;
|
||||
|
||||
static PENDING_PLUGIN_CALLS: OnceCell<Mutex<HashMap<i32, PendingPluginCallHandler>>> =
|
||||
OnceCell::new();
|
||||
static CHANNELS: OnceCell<Mutex<HashMap<u32, Channel>>> = OnceCell::new();
|
||||
|
||||
/// Possible errors when invoking a plugin.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -42,6 +44,17 @@ pub enum PluginInvokeError {
|
||||
/// Failed to deserialize response.
|
||||
#[error("failed to deserialize response: {0}")]
|
||||
CannotDeserializeResponse(serde_json::Error),
|
||||
/// Failed to serialize request payload.
|
||||
#[error("failed to serialize payload: {0}")]
|
||||
CannotSerializePayload(serde_json::Error),
|
||||
}
|
||||
|
||||
pub(crate) fn register_channel(channel: Channel) {
|
||||
CHANNELS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(channel.id(), channel);
|
||||
}
|
||||
|
||||
/// Glue between Rust and the Kotlin code that sends the plugin response back.
|
||||
@@ -86,6 +99,26 @@ pub fn handle_android_plugin_response(
|
||||
}
|
||||
}
|
||||
|
||||
/// Glue between Rust and the Kotlin code that sends the channel data.
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn send_channel_data(
|
||||
env: jni::JNIEnv<'_>,
|
||||
channel_id: i64,
|
||||
data_str: jni::objects::JString<'_>,
|
||||
) {
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(env.get_string(data_str).unwrap().to_str().unwrap()).unwrap();
|
||||
|
||||
if let Some(channel) = CHANNELS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(channel_id as u32))
|
||||
{
|
||||
let _ = channel.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error response from the Kotlin and Swift backends.
|
||||
#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)]
|
||||
pub struct ErrorResponse<T = ()> {
|
||||
@@ -236,81 +269,22 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
|
||||
}
|
||||
|
||||
impl<R: Runtime> PluginHandle<R> {
|
||||
/// Executes the given mobile method.
|
||||
pub fn run_mobile_plugin<T: serde::de::DeserializeOwned>(
|
||||
/// Executes the given mobile command.
|
||||
pub fn run_mobile_plugin<T: DeserializeOwned>(
|
||||
&self,
|
||||
method: impl AsRef<str>,
|
||||
payload: impl serde::Serialize,
|
||||
command: impl AsRef<str>,
|
||||
payload: impl Serialize,
|
||||
) -> Result<T, PluginInvokeError> {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
self.run_ios_plugin(method, payload).map_err(Into::into)
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
self.run_android_plugin(method, payload).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
// Executes the given iOS method.
|
||||
#[cfg(target_os = "ios")]
|
||||
fn run_ios_plugin<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: impl AsRef<str>,
|
||||
payload: impl serde::Serialize,
|
||||
) -> Result<T, PluginInvokeError> {
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
os::raw::{c_char, c_int},
|
||||
};
|
||||
|
||||
let id: i32 = rand::random();
|
||||
let (tx, rx) = channel();
|
||||
PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(
|
||||
id,
|
||||
Box::new(move |arg| {
|
||||
tx.send(arg).unwrap();
|
||||
}),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
extern "C" fn plugin_method_response_handler(
|
||||
id: c_int,
|
||||
success: c_int,
|
||||
payload: *const c_char,
|
||||
) {
|
||||
let payload = unsafe {
|
||||
assert!(!payload.is_null());
|
||||
CStr::from_ptr(payload)
|
||||
};
|
||||
|
||||
if let Some(handler) = PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&id)
|
||||
{
|
||||
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
|
||||
handler(if success == 1 {
|
||||
Ok(payload)
|
||||
} else {
|
||||
Err(payload)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
crate::ios::run_plugin_method(
|
||||
id,
|
||||
&self.name.into(),
|
||||
&method.as_ref().into(),
|
||||
crate::ios::json_to_dictionary(&serde_json::to_value(payload).unwrap()) as _,
|
||||
crate::ios::PluginMessageCallback(plugin_method_response_handler),
|
||||
);
|
||||
}
|
||||
run_command(
|
||||
self.name,
|
||||
&self.handle,
|
||||
command,
|
||||
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
|
||||
move |response| {
|
||||
tx.send(response).unwrap();
|
||||
},
|
||||
)?;
|
||||
|
||||
let response = rx.recv().unwrap();
|
||||
match response {
|
||||
@@ -322,89 +296,154 @@ impl<R: Runtime> PluginHandle<R> {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Executes the given Android method.
|
||||
#[cfg(target_os = "android")]
|
||||
fn run_android_plugin<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: impl AsRef<str>,
|
||||
payload: impl serde::Serialize,
|
||||
) -> Result<T, PluginInvokeError> {
|
||||
use jni::{errors::Error as JniError, objects::JObject, JNIEnv};
|
||||
|
||||
fn run<R: Runtime>(
|
||||
id: i32,
|
||||
plugin: &'static str,
|
||||
method: String,
|
||||
payload: &serde_json::Value,
|
||||
runtime_handle: &R::Handle,
|
||||
env: JNIEnv<'_>,
|
||||
activity: JObject<'_>,
|
||||
) -> Result<(), JniError> {
|
||||
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
|
||||
let plugin_manager = env
|
||||
.call_method(
|
||||
activity,
|
||||
"getPluginManager",
|
||||
"()Lapp/tauri/plugin/PluginManager;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
env.call_method(
|
||||
plugin_manager,
|
||||
"runCommand",
|
||||
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
|
||||
&[
|
||||
id.into(),
|
||||
env.new_string(plugin)?.into(),
|
||||
env.new_string(&method)?.into(),
|
||||
data,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let handle = match self.handle.runtime() {
|
||||
RuntimeOrDispatch::Runtime(r) => r.handle(),
|
||||
RuntimeOrDispatch::RuntimeHandle(h) => h,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let id: i32 = rand::random();
|
||||
let plugin_name = self.name;
|
||||
let method = method.as_ref().to_string();
|
||||
let payload = serde_json::to_value(payload).unwrap();
|
||||
let handle_ = handle.clone();
|
||||
|
||||
let (tx, rx) = channel();
|
||||
let tx_ = tx.clone();
|
||||
PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(
|
||||
id,
|
||||
Box::new(move |arg| {
|
||||
tx.send(Ok(arg)).unwrap();
|
||||
}),
|
||||
);
|
||||
|
||||
handle.run_on_android_context(move |env, activity, _webview| {
|
||||
if let Err(e) = run::<R>(id, plugin_name, method, &payload, &handle_, env, activity) {
|
||||
tx_.send(Err(e)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let response = rx.recv().unwrap()?;
|
||||
match response {
|
||||
Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse),
|
||||
Err(r) => Err(
|
||||
serde_json::from_value::<ErrorResponse>(r)
|
||||
.map(Into::into)
|
||||
.map_err(PluginInvokeError::CannotDeserializeResponse)?,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) fn run_command<R: Runtime, C: AsRef<str>, F: FnOnce(PluginResponse) + Send + 'static>(
|
||||
name: &str,
|
||||
_handle: &AppHandle<R>,
|
||||
command: C,
|
||||
payload: serde_json::Value,
|
||||
handler: F,
|
||||
) -> Result<(), PluginInvokeError> {
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
os::raw::{c_char, c_int, c_ulonglong},
|
||||
};
|
||||
|
||||
let id: i32 = rand::random();
|
||||
PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
unsafe {
|
||||
extern "C" fn plugin_command_response_handler(
|
||||
id: c_int,
|
||||
success: c_int,
|
||||
payload: *const c_char,
|
||||
) {
|
||||
let payload = unsafe {
|
||||
assert!(!payload.is_null());
|
||||
CStr::from_ptr(payload)
|
||||
};
|
||||
|
||||
if let Some(handler) = PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&id)
|
||||
{
|
||||
let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap();
|
||||
handler(if success == 1 {
|
||||
Ok(payload)
|
||||
} else {
|
||||
Err(payload)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) {
|
||||
let payload = unsafe {
|
||||
assert!(!payload.is_null());
|
||||
CStr::from_ptr(payload)
|
||||
};
|
||||
|
||||
if let Some(channel) = CHANNELS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(id as u32))
|
||||
{
|
||||
let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap();
|
||||
let _ = channel.send(payload);
|
||||
}
|
||||
}
|
||||
|
||||
crate::ios::run_plugin_command(
|
||||
id,
|
||||
&name.into(),
|
||||
&command.as_ref().into(),
|
||||
crate::ios::json_to_dictionary(&payload) as _,
|
||||
crate::ios::PluginMessageCallback(plugin_command_response_handler),
|
||||
crate::ios::ChannelSendDataCallback(send_channel_data_handler),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(crate) fn run_command<
|
||||
R: Runtime,
|
||||
C: AsRef<str>,
|
||||
F: FnOnce(PluginResponse) + Send + Clone + 'static,
|
||||
>(
|
||||
name: &str,
|
||||
handle: &AppHandle<R>,
|
||||
command: C,
|
||||
payload: serde_json::Value,
|
||||
handler: F,
|
||||
) -> Result<(), PluginInvokeError> {
|
||||
use jni::{errors::Error as JniError, objects::JObject, JNIEnv};
|
||||
|
||||
fn run<R: Runtime>(
|
||||
id: i32,
|
||||
plugin: &str,
|
||||
command: String,
|
||||
payload: &serde_json::Value,
|
||||
runtime_handle: &R::Handle,
|
||||
env: JNIEnv<'_>,
|
||||
activity: JObject<'_>,
|
||||
) -> Result<(), JniError> {
|
||||
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
|
||||
let plugin_manager = env
|
||||
.call_method(
|
||||
activity,
|
||||
"getPluginManager",
|
||||
"()Lapp/tauri/plugin/PluginManager;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
env.call_method(
|
||||
plugin_manager,
|
||||
"runCommand",
|
||||
"(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
|
||||
&[
|
||||
id.into(),
|
||||
env.new_string(plugin)?.into(),
|
||||
env.new_string(&command)?.into(),
|
||||
data,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let handle = match handle.runtime() {
|
||||
RuntimeOrDispatch::Runtime(r) => r.handle(),
|
||||
RuntimeOrDispatch::RuntimeHandle(h) => h,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let id: i32 = rand::random();
|
||||
let plugin_name = name.to_string();
|
||||
let command = command.as_ref().to_string();
|
||||
let handle_ = handle.clone();
|
||||
|
||||
PENDING_PLUGIN_CALLS
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler.clone()));
|
||||
|
||||
handle.run_on_android_context(move |env, activity, _webview| {
|
||||
if let Err(e) = run::<R>(id, &plugin_name, command, &payload, &handle_, env, activity) {
|
||||
handler(Err(e.to_string().into()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -168,9 +168,10 @@ impl Scope {
|
||||
mod tests {
|
||||
use super::RemoteDomainAccessScope;
|
||||
use crate::{
|
||||
api::ipc::CallbackFn,
|
||||
ipc::CallbackFn,
|
||||
test::{assert_ipc_response, mock_app, MockRuntime},
|
||||
App, InvokePayload, Manager, Window, WindowBuilder,
|
||||
window::InvokeRequest,
|
||||
App, Manager, Window, WindowBuilder,
|
||||
};
|
||||
|
||||
const PLUGIN_NAME: &str = "test";
|
||||
@@ -188,7 +189,7 @@ mod tests {
|
||||
(app, window)
|
||||
}
|
||||
|
||||
fn path_is_absolute_payload() -> InvokePayload {
|
||||
fn path_is_absolute_request() -> InvokeRequest {
|
||||
let callback = CallbackFn(0);
|
||||
let error = CallbackFn(1);
|
||||
|
||||
@@ -198,23 +199,25 @@ mod tests {
|
||||
serde_json::Value::String(std::env::current_dir().unwrap().display().to_string()),
|
||||
);
|
||||
|
||||
InvokePayload {
|
||||
InvokeRequest {
|
||||
cmd: "plugin:path|is_absolute".into(),
|
||||
callback,
|
||||
error,
|
||||
inner: serde_json::Value::Object(payload),
|
||||
body: serde_json::Value::Object(payload).into(),
|
||||
headers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_test_payload() -> InvokePayload {
|
||||
fn plugin_test_request() -> InvokeRequest {
|
||||
let callback = CallbackFn(0);
|
||||
let error = CallbackFn(1);
|
||||
|
||||
InvokePayload {
|
||||
InvokeRequest {
|
||||
cmd: format!("plugin:{PLUGIN_NAME}|doSomething"),
|
||||
callback,
|
||||
error,
|
||||
inner: Default::default(),
|
||||
body: Default::default(),
|
||||
headers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +230,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(&crate::window::ipc_scope_not_found_error_message(
|
||||
"main",
|
||||
"https://tauri.app/",
|
||||
@@ -244,7 +247,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(&crate::window::ipc_scope_window_error_message("main")),
|
||||
);
|
||||
}
|
||||
@@ -258,7 +261,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(&crate::window::ipc_scope_domain_error_message(
|
||||
"https://tauri.app/",
|
||||
)),
|
||||
@@ -277,25 +280,25 @@ mod tests {
|
||||
]);
|
||||
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
|
||||
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
|
||||
|
||||
window.navigate("https://blog.tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(&crate::window::ipc_scope_domain_error_message(
|
||||
"https://blog.tauri.app/",
|
||||
)),
|
||||
);
|
||||
|
||||
window.navigate("https://sub.tauri.app".parse().unwrap());
|
||||
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
|
||||
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
|
||||
|
||||
window.window.label = "test".into();
|
||||
window.navigate("https://dev.tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(&crate::window::ipc_scope_not_found_error_message(
|
||||
"test",
|
||||
"https://dev.tauri.app/",
|
||||
@@ -310,7 +313,7 @@ mod tests {
|
||||
.add_plugin("path")]);
|
||||
|
||||
window.navigate("https://tauri.app/inner/path".parse().unwrap());
|
||||
assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
|
||||
assert_ipc_response(&window, path_is_absolute_request(), Ok(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,7 +325,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
path_is_absolute_payload(),
|
||||
path_is_absolute_request(),
|
||||
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
|
||||
);
|
||||
}
|
||||
@@ -336,7 +339,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
plugin_test_payload(),
|
||||
plugin_test_request(),
|
||||
Err(&format!("plugin {PLUGIN_NAME} not found")),
|
||||
);
|
||||
}
|
||||
@@ -350,7 +353,7 @@ mod tests {
|
||||
window.navigate("https://tauri.app".parse().unwrap());
|
||||
assert_ipc_response(
|
||||
&window,
|
||||
plugin_test_payload(),
|
||||
plugin_test_request(),
|
||||
Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
use crate::{
|
||||
command::{CommandArg, CommandItem},
|
||||
InvokeError, Runtime,
|
||||
ipc::InvokeError,
|
||||
Runtime,
|
||||
};
|
||||
use state::TypeMap;
|
||||
|
||||
|
||||
@@ -42,11 +42,12 @@
|
||||
//! // in this case we'll run the my_cmd command with no arguments
|
||||
//! tauri::test::assert_ipc_response(
|
||||
//! &window,
|
||||
//! tauri::InvokePayload {
|
||||
//! tauri::window::InvokeRequest {
|
||||
//! cmd: "my_cmd".into(),
|
||||
//! callback: tauri::api::ipc::CallbackFn(0),
|
||||
//! error: tauri::api::ipc::CallbackFn(1),
|
||||
//! inner: serde_json::Value::Null,
|
||||
//! callback: tauri::ipc::CallbackFn(0),
|
||||
//! error: tauri::ipc::CallbackFn(1),
|
||||
//! body: serde_json::Value::Null.into(),
|
||||
//! headers: Default::default(),
|
||||
//! },
|
||||
//! Ok(())
|
||||
//! );
|
||||
@@ -59,21 +60,19 @@
|
||||
mod mock_runtime;
|
||||
pub use mock_runtime::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
sync::{
|
||||
mpsc::{channel, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::hooks::window_invoke_responder;
|
||||
use crate::{api::ipc::CallbackFn, App, Builder, Context, InvokePayload, Manager, Pattern, Window};
|
||||
use crate::{
|
||||
ipc::{CallbackFn, InvokeResponse},
|
||||
window::InvokeRequest,
|
||||
App, Builder, Context, Pattern, Window,
|
||||
};
|
||||
use tauri_utils::{
|
||||
assets::{AssetKey, Assets, CspHash},
|
||||
config::{Config, PatternKind, TauriConfig},
|
||||
@@ -92,8 +91,6 @@ impl Hash for IpcKey {
|
||||
}
|
||||
}
|
||||
|
||||
struct Ipc(Mutex<HashMap<IpcKey, Sender<std::result::Result<JsonValue, JsonValue>>>>);
|
||||
|
||||
/// An empty [`Assets`] implementation.
|
||||
pub struct NoopAsset {
|
||||
csp_hashes: Vec<CspHash<'static>>,
|
||||
@@ -166,20 +163,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn mock_builder() -> Builder<MockRuntime> {
|
||||
let mut builder = Builder::<MockRuntime>::new().manage(Ipc(Default::default()));
|
||||
|
||||
builder.invoke_responder = Arc::new(|window, response, callback, error| {
|
||||
let window_ = window.clone();
|
||||
let ipc = window_.state::<Ipc>();
|
||||
let mut ipc_ = ipc.0.lock().unwrap();
|
||||
if let Some(tx) = ipc_.remove(&IpcKey { callback, error }) {
|
||||
tx.send(response.into_result()).unwrap();
|
||||
} else {
|
||||
window_invoke_responder(window, response, callback, error)
|
||||
}
|
||||
});
|
||||
|
||||
builder
|
||||
Builder::<MockRuntime>::new()
|
||||
}
|
||||
|
||||
/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].
|
||||
@@ -222,11 +206,12 @@ pub fn mock_app() -> App<MockRuntime> {
|
||||
/// // run the `ping` command and assert it returns `pong`
|
||||
/// tauri::test::assert_ipc_response(
|
||||
/// &window,
|
||||
/// tauri::InvokePayload {
|
||||
/// tauri::window::InvokeRequest {
|
||||
/// cmd: "ping".into(),
|
||||
/// callback: tauri::api::ipc::CallbackFn(0),
|
||||
/// error: tauri::api::ipc::CallbackFn(1),
|
||||
/// inner: serde_json::Value::Null,
|
||||
/// callback: tauri::ipc::CallbackFn(0),
|
||||
/// error: tauri::ipc::CallbackFn(1),
|
||||
/// body: serde_json::Value::Null.into(),
|
||||
/// headers: Default::default(),
|
||||
/// },
|
||||
/// // the expected response is a success with the "pong" payload
|
||||
/// // we could also use Err("error message") here to ensure the command failed
|
||||
@@ -237,18 +222,17 @@ pub fn mock_app() -> App<MockRuntime> {
|
||||
/// ```
|
||||
pub fn assert_ipc_response<T: Serialize + Debug>(
|
||||
window: &Window<MockRuntime>,
|
||||
payload: InvokePayload,
|
||||
request: InvokeRequest,
|
||||
expected: Result<T, T>,
|
||||
) {
|
||||
let callback = payload.callback;
|
||||
let error = payload.error;
|
||||
let ipc = window.state::<Ipc>();
|
||||
let (tx, rx) = channel();
|
||||
ipc.0.lock().unwrap().insert(IpcKey { callback, error }, tx);
|
||||
window.clone().on_message(payload).unwrap();
|
||||
let rx = window.clone().on_message(request);
|
||||
let response = rx.recv().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().unwrap(),
|
||||
match response {
|
||||
InvokeResponse::Ok(b) => Ok(b.into_json()),
|
||||
InvokeResponse::Err(e) => Err(e.0),
|
||||
},
|
||||
expected
|
||||
.map(|e| serde_json::to_value(e).unwrap())
|
||||
.map_err(|e| serde_json::to_value(e).unwrap())
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
pub(crate) mod menu;
|
||||
|
||||
use http::HeaderMap;
|
||||
pub use menu::{MenuEvent, MenuHandle};
|
||||
pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState};
|
||||
use url::Url;
|
||||
@@ -13,11 +14,12 @@ use url::Url;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::TitleBarStyle;
|
||||
use crate::{
|
||||
api::ipc::CallbackFn,
|
||||
app::AppHandle,
|
||||
command::{CommandArg, CommandItem},
|
||||
event::{Event, EventHandler},
|
||||
hooks::{InvokePayload, InvokeResponder},
|
||||
ipc::{
|
||||
CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, InvokeResponse,
|
||||
},
|
||||
manager::WindowManager,
|
||||
runtime::{
|
||||
http::{Request as HttpRequest, Response as HttpResponse},
|
||||
@@ -32,8 +34,7 @@ use crate::{
|
||||
sealed::ManagerBase,
|
||||
sealed::RuntimeOrDispatch,
|
||||
utils::config::{WindowConfig, WindowEffectsConfig, WindowUrl},
|
||||
EventLoopMessage, Invoke, InvokeError, InvokeMessage, InvokeResolver, Manager, PageLoadPayload,
|
||||
Runtime, Theme, WindowEvent,
|
||||
EventLoopMessage, Manager, Runtime, Theme, WindowEvent,
|
||||
};
|
||||
#[cfg(desktop)]
|
||||
use crate::{
|
||||
@@ -56,11 +57,16 @@ use std::{
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
mpsc::{sync_channel, Receiver},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
|
||||
pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
|
||||
pub(crate) type UriSchemeProtocolHandler =
|
||||
Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>;
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WindowCreatedEvent {
|
||||
@@ -772,6 +778,21 @@ struct JsEventListenerKey {
|
||||
pub event: String,
|
||||
}
|
||||
|
||||
/// The IPC invoke request.
|
||||
#[derive(Debug)]
|
||||
pub struct InvokeRequest {
|
||||
/// The invoke command.
|
||||
pub cmd: String,
|
||||
/// The success callback.
|
||||
pub callback: CallbackFn,
|
||||
/// The error callback.
|
||||
pub error: CallbackFn,
|
||||
/// The body of the request.
|
||||
pub body: InvokeBody,
|
||||
/// The request headers.
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
// TODO: expand these docs since this is a pretty important type
|
||||
/// A webview window managed by Tauri.
|
||||
///
|
||||
@@ -967,10 +988,6 @@ impl<R: Runtime> Window<R> {
|
||||
WindowBuilder::<'a, R>::new(manager, label.into(), url)
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
|
||||
self.manager.invoke_responder()
|
||||
}
|
||||
|
||||
/// The current window's dispatcher.
|
||||
pub(crate) fn dispatcher(&self) -> R::Dispatcher {
|
||||
self.window.dispatcher.clone()
|
||||
@@ -1662,14 +1679,17 @@ impl<R: Runtime> Window<R> {
|
||||
}
|
||||
|
||||
fn is_local_url(&self, current_url: &Url) -> bool {
|
||||
self.manager.get_url().make_relative(current_url).is_some() || {
|
||||
let protocol_url = self.manager.protocol_url();
|
||||
current_url.scheme() == protocol_url.scheme() && current_url.domain() == protocol_url.domain()
|
||||
}
|
||||
self.manager.get_url().make_relative(current_url).is_some()
|
||||
|| {
|
||||
let protocol_url = self.manager.protocol_url();
|
||||
current_url.scheme() == protocol_url.scheme()
|
||||
&& current_url.domain() == protocol_url.domain()
|
||||
}
|
||||
|| (cfg!(dev) && current_url.domain() == Some("tauri.localhost"))
|
||||
}
|
||||
|
||||
/// Handles this window receiving an [`InvokeMessage`].
|
||||
pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> {
|
||||
/// Handles this window receiving an [`InvokeRequest`].
|
||||
pub fn on_message(self, request: InvokeRequest) -> Receiver<InvokeResponse> {
|
||||
let manager = self.manager.clone();
|
||||
let current_url = self.url();
|
||||
let is_local = self.is_local_url(¤t_url);
|
||||
@@ -1691,39 +1711,108 @@ impl<R: Runtime> Window<R> {
|
||||
}
|
||||
}
|
||||
};
|
||||
match payload.cmd.as_str() {
|
||||
"__initialized" => {
|
||||
let payload: PageLoadPayload = serde_json::from_value(payload.inner)?;
|
||||
manager.run_on_page_load(self, payload);
|
||||
}
|
||||
_ => {
|
||||
let message = InvokeMessage::new(
|
||||
self.clone(),
|
||||
manager.state(),
|
||||
payload.cmd.to_string(),
|
||||
payload.inner,
|
||||
);
|
||||
#[allow(clippy::redundant_clone)]
|
||||
let resolver = InvokeResolver::new(self.clone(), payload.callback, payload.error);
|
||||
|
||||
let mut invoke = Invoke { message, resolver };
|
||||
if !is_local && scope.is_none() {
|
||||
invoke.resolver.reject(scope_not_found_error_message);
|
||||
return Ok(());
|
||||
}
|
||||
let (tx, rx) = sync_channel(1);
|
||||
|
||||
if payload.cmd.starts_with("plugin:") {
|
||||
if !is_local {
|
||||
let command = invoke.message.command.replace("plugin:", "");
|
||||
let plugin_name = command.split('|').next().unwrap().to_string();
|
||||
if !scope
|
||||
.map(|s| s.plugins().contains(&plugin_name))
|
||||
.unwrap_or(true)
|
||||
let custom_responder = self.manager.invoke_responder();
|
||||
|
||||
let resolver = InvokeResolver::new(
|
||||
self.clone(),
|
||||
Arc::new(
|
||||
#[allow(unused_variables)]
|
||||
move |window: Window<R>, cmd, response, callback, error| {
|
||||
#[cfg(not(ipc_custom_protocol))]
|
||||
{
|
||||
use crate::ipc::{
|
||||
format_callback::{
|
||||
format as format_callback, format_result as format_callback_result,
|
||||
},
|
||||
Channel,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
// the channel data command is the only command that uses a custom protocol on Linux
|
||||
if custom_responder.is_none() && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND
|
||||
{
|
||||
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
|
||||
return Ok(());
|
||||
fn responder_eval<R: Runtime>(
|
||||
window: &Window<R>,
|
||||
js: crate::api::Result<String>,
|
||||
error: CallbackFn,
|
||||
) {
|
||||
let eval_js = match js {
|
||||
Ok(js) => js,
|
||||
Err(e) => format_callback(error, &e.to_string())
|
||||
.expect("unable to serialize response error string to json"),
|
||||
};
|
||||
|
||||
let _ = window.eval(&eval_js);
|
||||
}
|
||||
|
||||
match &response {
|
||||
InvokeResponse::Ok(InvokeBody::Json(v)) => {
|
||||
if matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) {
|
||||
let _ = Channel::from_ipc(window.clone(), callback).send(v);
|
||||
} else {
|
||||
responder_eval(
|
||||
&window,
|
||||
format_callback_result(Result::<_, ()>::Ok(v), callback, error),
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
InvokeResponse::Ok(InvokeBody::Raw(v)) => {
|
||||
let _ =
|
||||
Channel::from_ipc(window.clone(), callback).send(InvokeBody::Raw(v.clone()));
|
||||
}
|
||||
InvokeResponse::Err(e) => responder_eval(
|
||||
&window,
|
||||
format_callback_result(Result::<(), _>::Err(&e.0), callback, error),
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(responder) = &custom_responder {
|
||||
(responder)(window, cmd, &response, callback, error);
|
||||
}
|
||||
|
||||
let _ = tx.send(response);
|
||||
},
|
||||
),
|
||||
request.cmd.clone(),
|
||||
request.callback,
|
||||
request.error,
|
||||
);
|
||||
|
||||
match request.cmd.as_str() {
|
||||
"__initialized" => match request.body.deserialize() {
|
||||
Ok(payload) => {
|
||||
manager.run_on_page_load(self, payload);
|
||||
resolver.resolve(());
|
||||
}
|
||||
Err(e) => resolver.reject(e.to_string()),
|
||||
},
|
||||
_ => {
|
||||
#[cfg(mobile)]
|
||||
let app_handle = self.app_handle.clone();
|
||||
|
||||
let message = InvokeMessage::new(
|
||||
self,
|
||||
manager.state(),
|
||||
request.cmd.to_string(),
|
||||
request.body,
|
||||
request.headers,
|
||||
);
|
||||
|
||||
let mut invoke = Invoke {
|
||||
message,
|
||||
resolver: resolver.clone(),
|
||||
};
|
||||
|
||||
if !is_local && scope.is_none() {
|
||||
invoke.resolver.reject(scope_not_found_error_message);
|
||||
} else if request.cmd.starts_with("plugin:") {
|
||||
let command = invoke.message.command.replace("plugin:", "");
|
||||
let mut tokens = command.split('|');
|
||||
// safe to unwrap: split always has a least one item
|
||||
@@ -1733,101 +1822,59 @@ impl<R: Runtime> Window<R> {
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
if !(is_local
|
||||
|| plugin == crate::ipc::channel::CHANNEL_PLUGIN_NAME
|
||||
|| scope
|
||||
.map(|s| s.plugins().contains(&plugin.into()))
|
||||
.unwrap_or(true))
|
||||
{
|
||||
invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW);
|
||||
return rx;
|
||||
}
|
||||
|
||||
let command = invoke.message.command.clone();
|
||||
let resolver = invoke.resolver.clone();
|
||||
|
||||
#[cfg(mobile)]
|
||||
let message = invoke.message.clone();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut handled = manager.extend_api(plugin, invoke);
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
#[cfg(mobile)]
|
||||
{
|
||||
if !handled {
|
||||
handled = true;
|
||||
let plugin = plugin.to_string();
|
||||
let (callback, error) = (resolver.callback, resolver.error);
|
||||
self.with_webview(move |webview| {
|
||||
unsafe {
|
||||
crate::ios::post_ipc_message(
|
||||
webview.inner() as _,
|
||||
&plugin.as_str().into(),
|
||||
&heck::ToLowerCamelCase::to_lower_camel_case(message.command.as_str())
|
||||
.as_str()
|
||||
.into(),
|
||||
crate::ios::json_to_dictionary(&message.payload) as _,
|
||||
callback.0,
|
||||
error.0,
|
||||
)
|
||||
};
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if !handled {
|
||||
handled = true;
|
||||
fn load_channels<R: Runtime>(payload: &serde_json::Value, window: &Window<R>) {
|
||||
if let serde_json::Value::Object(map) = payload {
|
||||
for v in map.values() {
|
||||
if let serde_json::Value::String(s) = v {
|
||||
if s.starts_with(crate::ipc::channel::IPC_PAYLOAD_PREFIX) {
|
||||
crate::ipc::Channel::load_from_ipc(window.clone(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let payload = message.payload.into_json();
|
||||
// initialize channels
|
||||
load_channels(&payload, &message.window);
|
||||
|
||||
let resolver_ = resolver.clone();
|
||||
let runtime_handle = self.app_handle.runtime_handle.clone();
|
||||
let plugin = plugin.to_string();
|
||||
self.with_webview(move |webview| {
|
||||
webview.jni_handle().exec(move |env, activity, webview| {
|
||||
use jni::{
|
||||
errors::Error as JniError,
|
||||
objects::JObject,
|
||||
JNIEnv,
|
||||
};
|
||||
|
||||
fn handle_message<R: Runtime>(
|
||||
plugin: &str,
|
||||
runtime_handle: &R::Handle,
|
||||
message: InvokeMessage<R>,
|
||||
(callback, error): (CallbackFn, CallbackFn),
|
||||
env: JNIEnv<'_>,
|
||||
activity: JObject<'_>,
|
||||
webview: JObject<'_>,
|
||||
) -> Result<(), JniError> {
|
||||
let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, &message.payload)?;
|
||||
let plugin_manager = env
|
||||
.call_method(
|
||||
activity,
|
||||
"getPluginManager",
|
||||
"()Lapp/tauri/plugin/PluginManager;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
env.call_method(
|
||||
plugin_manager,
|
||||
"postIpcMessage",
|
||||
"(Landroid/webkit/WebView;Ljava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;JJ)V",
|
||||
&[
|
||||
webview.into(),
|
||||
env.new_string(plugin)?.into(),
|
||||
env.new_string(&heck::ToLowerCamelCase::to_lower_camel_case(message.command.as_str()))?.into(),
|
||||
data,
|
||||
(callback.0 as i64).into(),
|
||||
(error.0 as i64).into(),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Err(e) = handle_message(
|
||||
&plugin,
|
||||
&runtime_handle,
|
||||
message,
|
||||
(resolver_.callback, resolver_.error),
|
||||
env,
|
||||
activity,
|
||||
webview,
|
||||
) {
|
||||
resolver_.reject(format!("failed to reach Android layer: {e}"));
|
||||
}
|
||||
});
|
||||
})?;
|
||||
if let Err(e) = crate::plugin::mobile::run_command(
|
||||
plugin,
|
||||
&app_handle,
|
||||
message.command,
|
||||
payload,
|
||||
move |response| match response {
|
||||
Ok(r) => resolver_.resolve(r),
|
||||
Err(e) => resolver_.reject(e),
|
||||
},
|
||||
) {
|
||||
resolver.reject(e.to_string());
|
||||
return rx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1836,7 +1883,6 @@ impl<R: Runtime> Window<R> {
|
||||
}
|
||||
} else {
|
||||
let command = invoke.message.command.clone();
|
||||
let resolver = invoke.resolver.clone();
|
||||
let handled = manager.run_invoke_handler(invoke);
|
||||
if !handled {
|
||||
resolver.reject(format!("Command {command} not found"));
|
||||
@@ -1845,7 +1891,7 @@ impl<R: Runtime> Window<R> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
rx
|
||||
}
|
||||
|
||||
/// Evaluates JavaScript on this window.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
examples/api/dist/assets/index.js
vendored
14
examples/api/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -2,6 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
|
||||
window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
|
||||
return payload
|
||||
}
|
||||
|
||||
5
examples/api/src-tauri/Cargo.lock
generated
5
examples/api/src-tauri/Cargo.lock
generated
@@ -3243,9 +3243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.21.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b87728a671df8520c274fa9bed48d7384f5a965ef2fc364f01a942f6ff1ae6d2"
|
||||
checksum = "436fb014010f6c87561125b14add8a6091354681f190bb9ffeb42819af9218a4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cairo-rs",
|
||||
@@ -3329,6 +3329,7 @@ dependencies = [
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -17,8 +17,15 @@ pub fn log_operation(event: String, payload: Option<String>) {
|
||||
log::info!("{} {:?}", event, payload);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn perform_request(endpoint: String, body: RequestBody) -> String {
|
||||
println!("{} {:?}", endpoint, body);
|
||||
"message response".into()
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
|
||||
println!("{} {:?}", endpoint, body);
|
||||
ApiResponse {
|
||||
message: "message response".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ mod cmd;
|
||||
mod tray;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
|
||||
use tauri::{ipc::Channel, window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
|
||||
use tauri_plugin_sample::{PingRequest, SampleExt};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -66,6 +66,10 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
|
||||
let value = Some("test".to_string());
|
||||
let response = app.sample().ping(PingRequest {
|
||||
value: value.clone(),
|
||||
on_event: Channel::new(|event| {
|
||||
println!("got channel event: {:?}", event);
|
||||
Ok(())
|
||||
}),
|
||||
});
|
||||
log::info!("got response: {:?}", response);
|
||||
if let Ok(res) = response {
|
||||
|
||||
@@ -17,6 +17,11 @@ class ExamplePlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun ping(invoke: Invoke) {
|
||||
val onEvent = invoke.getChannel("onEvent")
|
||||
val event = JSObject()
|
||||
event.put("kind", "ping")
|
||||
onEvent?.send(event)
|
||||
|
||||
val value = invoke.getString("value") ?: ""
|
||||
val ret = JSObject()
|
||||
ret.put("value", implementation.pong(value))
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
import Tauri
|
||||
import SwiftRs
|
||||
|
||||
class ExamplePlugin: Plugin {
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let value = invoke.getString("value")
|
||||
invoke.resolve(["value": value as Any])
|
||||
}
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let onEvent = invoke.getChannel("onEvent")
|
||||
onEvent?.send(["kind": "ping"])
|
||||
|
||||
let value = invoke.getString("value")
|
||||
invoke.resolve(["value": value as Any])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_sample")
|
||||
func initPlugin() -> Plugin {
|
||||
return ExamplePlugin()
|
||||
return ExamplePlugin()
|
||||
}
|
||||
|
||||
@@ -17,8 +17,14 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
/// A helper class to access the sample APIs.
|
||||
pub struct Sample<R: Runtime>(AppHandle<R>);
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Event {
|
||||
kind: &'static str,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Sample<R> {
|
||||
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
|
||||
let _ = payload.on_event.send(Event { kind: "ping" });
|
||||
Ok(PingResponse {
|
||||
value: payload.value,
|
||||
})
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::ipc::Channel;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Serialize)]
|
||||
pub struct PingRequest {
|
||||
pub value: Option<String>,
|
||||
#[serde(rename = "onEvent")]
|
||||
pub on_event: Channel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
|
||||
@@ -26,7 +26,11 @@
|
||||
"name": "theme",
|
||||
"takesValue": true,
|
||||
"description": "App theme",
|
||||
"possibleValues": ["light", "dark", "system"]
|
||||
"possibleValues": [
|
||||
"light",
|
||||
"dark",
|
||||
"system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"short": "v",
|
||||
@@ -88,7 +92,10 @@
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"font-src": ["https://fonts.gstatic.com"],
|
||||
"connect-src": "ipc: https://ipc.localhost",
|
||||
"font-src": [
|
||||
"https://fonts.gstatic.com"
|
||||
],
|
||||
"img-src": "'self' asset: https://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
|
||||
},
|
||||
@@ -96,8 +103,13 @@
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
|
||||
"deny": ["$APPDATA/db/*.stronghold"]
|
||||
"allow": [
|
||||
"$APPDATA/db/**",
|
||||
"$RESOURCE/**"
|
||||
],
|
||||
"deny": [
|
||||
"$APPDATA/db/*.stronghold"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
window.__TAURI__
|
||||
.invoke(commandName, args)
|
||||
.then((response) => {
|
||||
result.innerText = `Ok(${response})`
|
||||
const val = response instanceof ArrayBuffer ? new TextDecoder().decode(response) : response
|
||||
result.innerText = `Ok(${val})`
|
||||
})
|
||||
.catch((error) => {
|
||||
result.innerText = `Err(${error})`
|
||||
@@ -28,6 +29,7 @@
|
||||
const container = document.querySelector('#container')
|
||||
const commands = [
|
||||
{ name: 'borrow_cmd' },
|
||||
{ name: 'raw_request' },
|
||||
{ name: 'window_label' },
|
||||
{ name: 'simple_command' },
|
||||
{ name: 'stateful_command' },
|
||||
|
||||
@@ -9,7 +9,11 @@ mod commands;
|
||||
use commands::{cmd, invoke, message, resolver};
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::{command, State, Window};
|
||||
use tauri::{
|
||||
command,
|
||||
ipc::{Request, Response},
|
||||
State, Window,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MyState {
|
||||
@@ -213,6 +217,12 @@ fn borrow_cmd_async(the_argument: &str) -> &str {
|
||||
the_argument
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn raw_request(request: Request<'_>) -> Response {
|
||||
println!("{:?}", request);
|
||||
Response::new(include_bytes!("./README.md").to_vec())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(MyState {
|
||||
@@ -222,6 +232,7 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
borrow_cmd,
|
||||
borrow_cmd_async,
|
||||
raw_request,
|
||||
window_label,
|
||||
force_async,
|
||||
force_async_with_result,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["index.html"],
|
||||
"devPath": ["index.html"],
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
@@ -47,7 +51,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["index.html"],
|
||||
"devPath": ["index.html"],
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
@@ -46,7 +50,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
|
||||
console.log('hook', payload)
|
||||
window.__TAURI_ISOLATION_HOOK__ = (payload, options) => {
|
||||
console.log('hook', payload, options)
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["index.html"],
|
||||
"devPath": ["index.html"],
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
@@ -43,7 +47,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["index.html"],
|
||||
"devPath": ["index.html"],
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
@@ -27,7 +31,7 @@
|
||||
"category": "DeveloperTool"
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["../index.html"],
|
||||
"devPath": ["../index.html"],
|
||||
"distDir": [
|
||||
"../index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"../index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
@@ -23,7 +27,9 @@
|
||||
"../../.icons/icon.icns",
|
||||
"../../.icons/icon.ico"
|
||||
],
|
||||
"resources": ["assets/*"],
|
||||
"resources": [
|
||||
"assets/*"
|
||||
],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "DeveloperTool",
|
||||
@@ -47,7 +53,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["index.html"],
|
||||
"devPath": ["index.html"],
|
||||
"distDir": [
|
||||
"index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"withGlobalTauri": true
|
||||
@@ -47,7 +51,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'"
|
||||
"csp": "default-src 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; media-src stream: https://stream.localhost asset: https://asset.localhost",
|
||||
"csp": "default-src 'self' ipc:; media-src stream: https://stream.localhost asset: https://asset.localhost",
|
||||
"assetProtocol": {
|
||||
"scope": ["**/test_video.mp4"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"$schema": "../../../core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"distDir": ["src/index.html"],
|
||||
"devPath": ["src/index.html"],
|
||||
"distDir": [
|
||||
"src/index.html"
|
||||
],
|
||||
"devPath": [
|
||||
"src/index.html"
|
||||
],
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
@@ -46,7 +50,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -41,10 +41,10 @@ interface IPCMessage {
|
||||
* })
|
||||
*
|
||||
* test("mocked command", () => {
|
||||
* mockIPC((cmd, args) => {
|
||||
* mockIPC((cmd, payload) => {
|
||||
* switch (cmd) {
|
||||
* case "add":
|
||||
* return (args.a as number) + (args.b as number);
|
||||
* return (payload.a as number) + (payload.b as number);
|
||||
* default:
|
||||
* break;
|
||||
* }
|
||||
@@ -64,7 +64,7 @@ interface IPCMessage {
|
||||
* })
|
||||
*
|
||||
* test("mocked command", () => {
|
||||
* mockIPC((cmd, args) => {
|
||||
* mockIPC((cmd, payload) => {
|
||||
* if(cmd === "get_data") {
|
||||
* return fetch("https://example.com/data.json")
|
||||
* .then((response) => response.json())
|
||||
@@ -78,19 +78,19 @@ interface IPCMessage {
|
||||
* @since 1.0.0
|
||||
*/
|
||||
export function mockIPC(
|
||||
cb: (cmd: string, args: Record<string, unknown>) => any | Promise<any>
|
||||
cb: (cmd: string, payload: Record<string, unknown>) => any | Promise<any>
|
||||
): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
window.__TAURI_IPC__ = async ({
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
...args
|
||||
payload
|
||||
}: IPCMessage) => {
|
||||
try {
|
||||
// @ts-expect-error The function key is dynamic and therefore not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
window[`_${callback}`](await cb(cmd, args))
|
||||
window[`_${callback}`](await cb(cmd, payload))
|
||||
} catch (err) {
|
||||
// @ts-expect-error The function key is dynamic and therefore not typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
|
||||
@@ -43,17 +43,6 @@ enum BaseDirectory {
|
||||
Template
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__: {
|
||||
path: {
|
||||
__sep: string
|
||||
__delimiter: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's config files.
|
||||
* Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the value [`tauri.bundle.identifier`](https://tauri.app/v1/api/config/#bundleconfig.identifier) is configured in `tauri.conf.json`.
|
||||
|
||||
@@ -11,12 +11,17 @@
|
||||
|
||||
/** @ignore */
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Window {
|
||||
__TAURI_IPC__: (message: any) => void
|
||||
ipc: {
|
||||
postMessage: (args: string) => void
|
||||
__TAURI__: {
|
||||
path: {
|
||||
__sep: string
|
||||
__delimiter: string
|
||||
}
|
||||
|
||||
convertFileSrc: (src: string, protocol: string) => string
|
||||
}
|
||||
|
||||
__TAURI_IPC__: (message: any) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +130,14 @@ async function addPluginListener<T>(
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
type InvokeArgs = Record<string, unknown>
|
||||
type InvokeArgs = Record<string, unknown> | number[] | ArrayBuffer | Uint8Array
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
interface InvokeOptions {
|
||||
headers: Headers | Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the backend.
|
||||
@@ -137,11 +149,16 @@ type InvokeArgs = Record<string, unknown>
|
||||
*
|
||||
* @param cmd The command name.
|
||||
* @param args The optional arguments to pass to the command.
|
||||
* @param options The request options.
|
||||
* @return A promise resolving or rejecting to the backend response.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
|
||||
async function invoke<T>(
|
||||
cmd: string,
|
||||
args: InvokeArgs = {},
|
||||
options?: InvokeOptions
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = transformCallback((e: T) => {
|
||||
resolve(e)
|
||||
@@ -156,7 +173,8 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
...args
|
||||
payload: args,
|
||||
options
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -164,7 +182,7 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
|
||||
/**
|
||||
* Convert a device file path to an URL that can be loaded by the webview.
|
||||
* Note that `asset:` and `https://asset.localhost` must be added to [`tauri.security.csp`](https://tauri.app/v1/api/config/#securityconfig.csp) in `tauri.conf.json`.
|
||||
* Example CSP value: `"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"` to use the asset protocol on image sources.
|
||||
* Example CSP value: `"csp": "default-src 'self' ipc: https://ipc.localhost; img-src 'self' asset: https://asset.localhost"` to use the asset protocol on image sources.
|
||||
*
|
||||
* Additionally, `asset` must be added to [`tauri.allowlist.protocol`](https://tauri.app/v1/api/config/#allowlistconfig.protocol)
|
||||
* in `tauri.conf.json` and its access scope must be defined on the `assetScope` array on the same `protocol` object.
|
||||
@@ -192,13 +210,10 @@ async function invoke<T>(cmd: string, args: InvokeArgs = {}): Promise<T> {
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function convertFileSrc(filePath: string, protocol = 'asset'): string {
|
||||
const path = encodeURIComponent(filePath)
|
||||
return navigator.userAgent.includes('Windows')
|
||||
? `https://${protocol}.localhost/${path}`
|
||||
: `${protocol}://localhost/${path}`
|
||||
return window.__TAURI__.convertFileSrc(filePath, protocol)
|
||||
}
|
||||
|
||||
export type { InvokeArgs }
|
||||
export type { InvokeArgs, InvokeOptions }
|
||||
|
||||
export {
|
||||
transformCallback,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::fs::read;
|
||||
use tauri::{command, path::BaseDirectory, AppHandle, Manager, Runtime};
|
||||
use tauri::{command, ipc::Response, path::BaseDirectory, AppHandle, Manager, Runtime};
|
||||
|
||||
#[command]
|
||||
fn app_should_close(exit_code: i32) {
|
||||
@@ -13,13 +13,13 @@ fn app_should_close(exit_code: i32) {
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Vec<u8>, String> {
|
||||
async fn read_file<R: Runtime>(app: AppHandle<R>) -> Result<Response, String> {
|
||||
let path = app
|
||||
.path()
|
||||
.resolve(".tauri_3mb.json", BaseDirectory::Home)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let contents = read(&path).map_err(|e| e.to_string())?;
|
||||
Ok(contents)
|
||||
Ok(Response::new(contents))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,13 @@ fn migrate_config(config: &mut Value) -> Result<()> {
|
||||
process_allowlist(tauri_config, &mut plugins, allowlist)?;
|
||||
}
|
||||
|
||||
if let Some(security) = tauri_config
|
||||
.get_mut("security")
|
||||
.and_then(|c| c.as_object_mut())
|
||||
{
|
||||
process_security(security)?;
|
||||
}
|
||||
|
||||
// cli
|
||||
if let Some(cli) = tauri_config.remove("cli") {
|
||||
process_cli(&mut plugins, cli)?;
|
||||
@@ -64,6 +71,44 @@ fn migrate_config(config: &mut Value) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_security(security: &mut Map<String, Value>) -> Result<()> {
|
||||
// migrate CSP: add `ipc:` to `connect-src`
|
||||
if let Some(csp_value) = security.remove("csp") {
|
||||
let csp = if csp_value.is_null() {
|
||||
csp_value
|
||||
} else {
|
||||
let mut csp: tauri_utils_v1::config::Csp = serde_json::from_value(csp_value)?;
|
||||
match &mut csp {
|
||||
tauri_utils_v1::config::Csp::Policy(csp) => {
|
||||
if csp.contains("connect-src") {
|
||||
*csp = csp.replace("connect-src", "connect-src ipc: https://ipc.localhost");
|
||||
} else {
|
||||
*csp = format!("{csp}; connect-src ipc: https://ipc.localhost");
|
||||
}
|
||||
}
|
||||
tauri_utils_v1::config::Csp::DirectiveMap(csp) => {
|
||||
if let Some(connect_src) = csp.get_mut("connect-src") {
|
||||
if !connect_src.contains("ipc: https://ipc.localhost") {
|
||||
connect_src.push("ipc: https://ipc.localhost");
|
||||
}
|
||||
} else {
|
||||
csp.insert(
|
||||
"connect-src".into(),
|
||||
tauri_utils_v1::config::CspDirectiveSources::List(vec![
|
||||
"ipc: https://ipc.localhost".to_string()
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_value(csp)?
|
||||
};
|
||||
|
||||
security.insert("csp".into(), csp);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_allowlist(
|
||||
tauri_config: &mut Map<String, Value>,
|
||||
plugins: &mut Map<String, Value>,
|
||||
@@ -142,8 +187,19 @@ fn process_updater(
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
fn migrate(original: &serde_json::Value) -> serde_json::Value {
|
||||
let mut migrated = original.clone();
|
||||
super::migrate_config(&mut migrated).expect("failed to migrate config");
|
||||
|
||||
if let Err(e) = serde_json::from_value::<tauri_utils::config::Config>(migrated.clone()) {
|
||||
panic!("migrated config is not valid: {e}");
|
||||
}
|
||||
|
||||
migrated
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate() {
|
||||
fn migrate_full() {
|
||||
let original = serde_json::json!({
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
@@ -199,16 +255,14 @@ mod test {
|
||||
"http": {
|
||||
"scope": ["http://localhost:3003/"]
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src: 'self' tauri:"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut migrated = original.clone();
|
||||
super::migrate_config(&mut migrated).expect("failed to migrate config");
|
||||
|
||||
if let Err(e) = serde_json::from_value::<tauri_utils::config::Config>(migrated.clone()) {
|
||||
panic!("migrated config is not valid: {e}");
|
||||
}
|
||||
let migrated = migrate(&original);
|
||||
|
||||
// bundle > updater
|
||||
assert_eq!(
|
||||
@@ -268,5 +322,105 @@ mod test {
|
||||
migrated["tauri"]["security"]["assetProtocol"]["scope"],
|
||||
original["tauri"]["allowlist"]["protocol"]["assetScope"]
|
||||
);
|
||||
|
||||
// security CSP
|
||||
assert_eq!(
|
||||
migrated["tauri"]["security"]["csp"],
|
||||
format!(
|
||||
"{}; connect-src ipc: https://ipc.localhost",
|
||||
original["tauri"]["security"]["csp"].as_str().unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_csp_object() {
|
||||
let original = serde_json::json!({
|
||||
"tauri": {
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": ["self", "tauri:"]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let migrated = migrate(&original);
|
||||
|
||||
assert_eq!(
|
||||
migrated["tauri"]["security"]["csp"]["default-src"],
|
||||
original["tauri"]["security"]["csp"]["default-src"]
|
||||
);
|
||||
assert!(migrated["tauri"]["security"]["csp"]["connect-src"]
|
||||
.as_array()
|
||||
.expect("connect-src isn't an array")
|
||||
.contains(&"ipc: https://ipc.localhost".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_csp_existing_connect_src_string() {
|
||||
let original = serde_json::json!({
|
||||
"tauri": {
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": ["self", "tauri:"],
|
||||
"connect-src": "self"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let migrated = migrate(&original);
|
||||
|
||||
assert_eq!(
|
||||
migrated["tauri"]["security"]["csp"]["default-src"],
|
||||
original["tauri"]["security"]["csp"]["default-src"]
|
||||
);
|
||||
assert_eq!(
|
||||
migrated["tauri"]["security"]["csp"]["connect-src"]
|
||||
.as_str()
|
||||
.expect("connect-src isn't a string"),
|
||||
format!(
|
||||
"{} ipc: https://ipc.localhost",
|
||||
original["tauri"]["security"]["csp"]["connect-src"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_csp_existing_connect_src_array() {
|
||||
let original = serde_json::json!({
|
||||
"tauri": {
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": ["self", "tauri:"],
|
||||
"connect-src": ["self", "asset:"]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let migrated = migrate(&original);
|
||||
|
||||
assert_eq!(
|
||||
migrated["tauri"]["security"]["csp"]["default-src"],
|
||||
original["tauri"]["security"]["csp"]["default-src"]
|
||||
);
|
||||
|
||||
let migrated_connect_src = migrated["tauri"]["security"]["csp"]["connect-src"]
|
||||
.as_array()
|
||||
.expect("connect-src isn't an array");
|
||||
let original_connect_src = original["tauri"]["security"]["csp"]["connect-src"]
|
||||
.as_array()
|
||||
.unwrap();
|
||||
assert!(
|
||||
migrated_connect_src
|
||||
.iter()
|
||||
.zip(original_connect_src.iter())
|
||||
.all(|(a, b)| a == b),
|
||||
"connect-src migration failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: https://ipc.localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user